blake.io/pqx@v0.2.2-0.20231231055241-83f2254c0a07/internal/backoff/backoff.go (about)

     1  // This was copied from tailscale.com/logtail/backoff and modified only enough
     2  // to break the dependencies on tailscale.
     3  
     4  // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
     5  // Use of this source code is governed by a BSD-style
     6  // license that can be found in the LICENSE file.
     7  
     8  // Package backoff provides a back-off timer type.
     9  package backoff
    10  
    11  import (
    12  	"context"
    13  	"math/rand"
    14  	"time"
    15  )
    16  
    17  // Backoff tracks state the history of consecutive failures and sleeps
    18  // an increasing amount of time, up to a provided limit.
    19  type Backoff struct {
    20  	n          int // number of consecutive failures
    21  	maxBackoff time.Duration
    22  
    23  	// Name is the name of this backoff timer, for logging purposes.
    24  	name string
    25  	// logf is the function used for log messages when backing off.
    26  	logf func(format string, args ...any)
    27  
    28  	// NewTimer is the function that acts like time.NewTimer.
    29  	// It's for use in unit tests.
    30  	NewTimer func(time.Duration) *time.Timer
    31  
    32  	// LogLongerThan sets the minimum time of a single backoff interval
    33  	// before we mention it in the log.
    34  	LogLongerThan time.Duration
    35  }
    36  
    37  // NewBackoff returns a new Backoff timer with the provided name (for logging), logger,
    38  // and max backoff time. By default, all failures (calls to BackOff with a non-nil err)
    39  // are logged unless the returned Backoff.LogLongerThan is adjusted.
    40  func NewBackoff(name string, logf func(string, ...any), maxBackoff time.Duration) *Backoff {
    41  	return &Backoff{
    42  		name:       name,
    43  		logf:       logf,
    44  		maxBackoff: maxBackoff,
    45  		NewTimer:   time.NewTimer,
    46  	}
    47  }
    48  
    49  // Backoff sleeps an increasing amount of time if err is non-nil.
    50  // and the context is not a
    51  // It resets the backoff schedule once err is nil.
    52  func (b *Backoff) BackOff(ctx context.Context, err error) {
    53  	if err == nil {
    54  		// No error. Reset number of consecutive failures.
    55  		b.n = 0
    56  		return
    57  	}
    58  	if ctx.Err() != nil {
    59  		// Fast path.
    60  		return
    61  	}
    62  
    63  	b.n++
    64  	// n^2 backoff timer is a little smoother than the
    65  	// common choice of 2^n.
    66  	d := time.Duration(b.n*b.n) * 10 * time.Millisecond
    67  	if d > b.maxBackoff {
    68  		d = b.maxBackoff
    69  	}
    70  	// Randomize the delay between 0.5-1.5 x msec, in order
    71  	// to prevent accidental "thundering herd" problems.
    72  	d = time.Duration(float64(d) * (rand.Float64() + 0.5))
    73  
    74  	if d >= b.LogLongerThan {
    75  		b.logf("%s: [v1] backoff: %d msec", b.name, d.Milliseconds())
    76  	}
    77  	t := b.NewTimer(d)
    78  	select {
    79  	case <-ctx.Done():
    80  		t.Stop()
    81  	case <-t.C:
    82  	}
    83  }