github.com/cilium/cilium@v1.16.2/pkg/inctimer/inctimer.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package inctimer
     5  
     6  import "time"
     7  
     8  // IncTimer should be the preferred mechanism over
     9  // calling `time.After` when wanting an `After`-like
    10  // function in a loop. This prevents memory build up
    11  // as the `time.After` method creates a new timer
    12  // instance every time it is called, and it is not
    13  // garbage collected until after it fires. Conversely,
    14  // IncTimer only uses one timer and correctly stops
    15  // the timer, clears its channel, and resets it
    16  // everytime that `After` is called.
    17  type IncTimer interface {
    18  	After(time.Duration) <-chan time.Time
    19  }
    20  
    21  type incTimer struct {
    22  	t *time.Timer
    23  }
    24  
    25  // New creates a new IncTimer and a done function.
    26  // IncTimer only uses one timer and correctly stops
    27  // the timer, clears the channel, and resets it every
    28  // time the `After` function is called.
    29  // WARNING: Concurrent use is not expected. The use
    30  // of this timer should be for only one goroutine.
    31  func New() (IncTimer, func() bool) {
    32  	it := &incTimer{}
    33  	return it, it.stop
    34  }
    35  
    36  // stop returns true if a scheduled timer has been stopped before execution.
    37  func (it *incTimer) stop() bool {
    38  	if it.t == nil {
    39  		return false
    40  	}
    41  	return it.t.Stop()
    42  }
    43  
    44  // After returns a channel that will fire after
    45  // the specified duration.
    46  func (it *incTimer) After(d time.Duration) <-chan time.Time {
    47  	// Stop the previous timer (if any) to garbage collect it.
    48  	// The old timer channel will be garbage collected even if not drained.
    49  	it.stop()
    50  
    51  	// We have to create a new timer for each invocation, because it is not
    52  	// possible to safely use https://golang.org/pkg/time/#Timer.Reset if we
    53  	// do not know if the timer channel has already been drained or not (which
    54  	// is the case here, as the client might have drained the channel already).
    55  	// Even after stopping a timer, it's not safe to attempt to drain its
    56  	// timer channel with a default case (for the case where the client has
    57  	// drained the channel already), as there is a small window where a timer
    58  	// is considered expired, but the channel has not received a value yet [1].
    59  	// This would cause us to erroneously take the default case (assuming the
    60  	// channel has been drained by the client), when in fact the channel just
    61  	// has not received a value yet. Because the two cases (client has drained
    62  	// vs. value not received yet) are indistinguishable for us, we cannot use
    63  	// Timer.Reset and need to create a new timer.
    64  	//
    65  	// [1] The reason why this small window occurs, is because the Go runtime
    66  	// will remove a timer from the heap and and mark it as deleted _before_
    67  	// it actually executes the timer function f:
    68  	// https://github.com/golang/go/blob/go1.16/src/runtime/time.go#L876
    69  	// This causes t.Stop to report the timer as already expired while it is
    70  	// in fact currently running:
    71  	// https://github.com/golang/go/blob/go1.16/src/runtime/time.go#L352
    72  	it.t = time.NewTimer(d)
    73  	return it.t.C
    74  }
    75  
    76  // After wraps the time.After function to get around the /timeafter linter
    77  // warning for cases where it is inconvenient to use the instantiated version.
    78  func After(d time.Duration) <-chan time.Time {
    79  	return time.After(d)
    80  }