Source File
debounce.go
Belonging Package
go.pact.im/x/flaky
package flaky
import (
)
// ErrDebounced is an error that DebounceExecutor returns when an Execute call
// is superseded by another operation.
var ErrDebounced = errors.New("flaky: debounced")
// DebounceExecutor is an executor that debounces an operation. That is, it
// performs an operation once it stops being called for the specified debounce
// duration.
//
// Keep in mind that using DebounceExecutor introduces latency to the operation
// for at least the specified debounce duration. In most cases it should be used
// when the operation is expensive to execute or under high event rate or load.
// As a side effect, DebounceExecutor guarantees that at most one operation is
// executing at a time.
//
type DebounceExecutor struct {
once sync.Once
debounceState
c *clock.Clock
w time.Duration
timer clock.Timer
}
// debounceState is the debounce state for DebounceExecutor.
type debounceState struct {
lock chan struct{}
exec chan struct{}
steal chan struct{}
next chan struct{}
}
// Debounce returns a new DebounceExecutor instance for the given wait duration.
func ( time.Duration) *DebounceExecutor {
return &DebounceExecutor{
w: ,
}
}
// clone returns a copy of the executor.
func ( *DebounceExecutor) () *DebounceExecutor {
.init()
return &DebounceExecutor{
debounceState: .debounceState,
c: .c,
w: .w,
timer: .timer,
}
}
// WithClock returns a copy of the executor that uses the given clock.
func ( *DebounceExecutor) ( *clock.Clock) *DebounceExecutor {
= .clone()
.timer = nil
.c =
.init()
return
}
// WithWait returns a copy of the executor that uses the given wait duration.
//
// It shares the underlying state and can be used to debounce operation with
// non-default wait duration.
func ( *DebounceExecutor) ( time.Duration) *DebounceExecutor {
= .clone()
.w =
.init()
return
}
// Execute calls the function f if and only if Execute is not called again
// during the debounce interval. Context expiration cancels an operation. It
// returns ErrDebounced error if the given operation f was superseded by another
// Execute call. If Execute is called concurrently, the last invocation wins.
// When another operation is already executing, it returns ErrDebounced.
//
// Callers should handle ErrDebounced error to avoid breaking assumptions when
// running under another DebounceExecutor.
//
// As a side effect, it guarantees that at most one f is executing at a time.
// That is, it is safe to avoid locking if the state is mutated exclusively
// under the DebounceExecutor.
func ( *DebounceExecutor) ( context.Context, Op) error {
.init()
// Acquire a lock or steal it from an ongoing debounced Execute call.
select {
case <-.Done():
return .Err()
case := <-.exec:
.exec <-
return ErrDebounced
case .lock <- struct{}{}:
case .steal <- struct{}{}:
// Wait until the Execute call we are stealing from stops the
// timer and passes the lock to us.
<-.next
}
// Reset timer for the debounce duration.
if .timer == nil {
.timer = .c.Timer(.w)
} else {
.timer.Reset(.w)
}
// Wait until the debounce timer expiration, another Execute invocation
// that steals our lock, or the action cancellation.
var bool
select {
case <-.timer.C():
.exec <- struct{}{}
:= ()
<-.exec
<-.lock
return unwrapInternal()
case <-.steal:
= true
case <-.Done():
}
if !.timer.Stop() {
<-.timer.C()
}
if {
.next <- struct{}{}
return ErrDebounced
}
<-.lock
return .Err()
}
// init initializes the internal debouncer state.
func ( *DebounceExecutor) () {
.once.Do(func() {
if .c == nil {
.c = clock.System()
}
if .lock == nil {
.lock = make(chan struct{}, 1)
}
if .exec == nil {
.exec = make(chan struct{}, 1)
}
if .steal == nil {
.steal = make(chan struct{})
}
if .next == nil {
.next = make(chan struct{})
}
})
}
The pages are generated with Golds v0.4.9. (GOOS=linux GOARCH=amd64)