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.
  • 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
              }
      }
      
Tags::cloud:cs: