Debounce Stability Pattern
Table of Contents
- etymological origins in electronic circuits : https://www.geeksforgeeks.org/switch-debounce-in-digital-circuits/
- limits the frequency of a function invocation so that only the first or last in a cluster of calls is actually performed.
- is native to javascript but can port to others as needed, will be proceeding in golang
- 2 components:
- Circuit : the computation to be regulated
- Debounce : A closure over Circuit that manages the calls
- similar logic to Circuit Breakers in that the closure maintains the rate limiting logic and state
1. Code
- on each call of the Debounce returned closure, regardless of the outcome, a time interval is set.
- calls before expiry of that duration are ignored, any after the duration are passed along to the inner Circuit function.
- this is a "function-first" : i.e cache results and ignore the latter calls
- Alternatively, a "funciton-last" implementation will accumulate a series of requests before calling Circuit
- This could be useful when the inner circuit needs some kick-starting corpus of inputs (think autocompletion)
- can be employed if the response can be delayed a little and increased latency is not an issue.
- calls before expiry of that duration are ignored, any after the duration are passed along to the inner Circuit function.
The Core Circuit can be a function as follows
type Circuit func(context.Context) (string, error)
The Debounce prepped closure can then be structured as follows (function-first)
func DebounceFirst(circuit Circuit, d time.Duration) Circuit { var threshold time.Time var result string // result cache var err error var m sync.Mutex return func(ctx context.Context) (string, error) { m.Lock() defer func() { threshold = time.Now().Add(d) m.Unlock() }() if time.Now().Before(threshold){ //return cached result before threshold return result, err } // if expired, compute and cache result // in the enclosed variable result result, err = circuit(ctx) return result, err } }
a function-last implementation needs a little more book-keeping
func DebounceLast(circuit Circuit, d time.Duration) Circuit { var threshold time.Time = time.Now() var ticker *time.Ticker var result string var err error var once sync.Once var m sync.Mutex return func(ctx context.Context) (string, error) { m.Lock() defer m.Unlock() threshold = time.Now().Add(d) once.Do(func() { ticker = time.NewTicker(time.Millisecond * 100) go func() { defer func() { m.Lock() ticker.Stop() once = sync.Once{} m.Unlock() }() for { select { case <-ticker.C: m.Lock() if time.Now().After(threshold) { result, err = circuit(ctx) m.Unlock() return } m.Unlock() case <-ctx.Done(): m.Lock() result, err = "", ctx.Err() m.Unlock() return } } }() }) return result, err } }