diff --git a/go.mod b/go.mod index 136c378..3fa22a5 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,10 @@ require ( ) require ( + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/yabackoff/exponential.go b/yabackoff/exponential.go new file mode 100644 index 0000000..e85381a --- /dev/null +++ b/yabackoff/exponential.go @@ -0,0 +1,119 @@ +package yabackoff + +import "time" + +// Exponential is a back‑off that multiplies the delay by a constant factor +// each time Next() is called, capping at maxInterval. +// +// Example: +// +// backoff := yabackoff.NewExponential(100*time.Millisecond, 2, time.Second) +// fmt.Println(backoff.Next()) // 200 ms +// fmt.Println(backoff.Next()) // 400 ms +// fmt.Println(backoff.Next()) // 800 ms +// fmt.Println(backoff.Next()) // 1 s (capped) +// fmt.Println(backoff.Next()) // 1 s (stays capped) +// +// The zero value of Exponential is usable: on first use the package defaults +// are substituted. +type Exponential struct { + initialInterval time.Duration + multiplier float64 + maxInterval time.Duration + currentInterval time.Duration +} + +// NewExponential creates a new exponential back‑off. Any zero argument is +// replaced by the corresponding package default. +// +// Example: +// +// backoff := yabackoff.NewExponential(0, 0, 0) // uses all defaults +// fmt.Println(backoff.Current()) // 500 ms (default) +func NewExponential( + initialInterval time.Duration, + multiplier float64, + maxInterval time.Duration, +) Exponential { + return Exponential{ + initialInterval: initialInterval, + multiplier: multiplier, + maxInterval: maxInterval, + currentInterval: initialInterval, + } +} + +// Reset sets currentInterval back to the initial value. +// +// Example: +// +// backoff := yabackoff.NewExponential(250*time.Millisecond, 2, time.Second) +// _ = backoff.Next() // 500 ms +// backoff.Reset() +// fmt.Println(backoff.Current()) // 250 ms +func (e *Exponential) Reset() { + e.currentInterval = e.initialInterval +} + +// Next returns the next delay and advances the internal state. +// +// Example: +// +// backoff := yabackoff.NewExponential(100*time.Millisecond, 2, time.Second) +// delay := backoff.Next() // 200 ms +// doSomethingAfter(delay) +func (e *Exponential) Next() time.Duration { + e.safety() + + e.incrementCurrentInterval() + + return e.currentInterval +} + +// Current reports the delay that would be (or was) returned by the most recent +// call to Next(). Calling Current() never mutates state. +// +// Example: +// +// fmt.Println("current delay:", backoff.Current()) +func (e *Exponential) Current() time.Duration { + return e.currentInterval +} + +// Wait sleeps for Next(). It is shorthand for `time.Sleep(b.Next())`. +// +// Example: +// +// start := time.Now() +// backoff.Wait() +// fmt.Println("slept for", time.Since(start)) +func (e *Exponential) Wait() { + time.Sleep(e.Next()) +} + +// incrementCurrentInterval multiplies currentInterval by multiplier, clamping +// at maxInterval. +func (e *Exponential) incrementCurrentInterval() { + if float64(e.currentInterval) >= float64(e.maxInterval) { + e.currentInterval = e.maxInterval + } else { + e.currentInterval = min(time.Duration(float64(e.currentInterval)*e.multiplier), e.maxInterval) + } +} + +// safety lazily substitutes defaults the first time the struct is used, so a +// zero value Exponential is fully functional. +func (e *Exponential) safety() { + if e.initialInterval == 0 { + e.initialInterval = DefaultInitialInterval + e.currentInterval = DefaultInitialInterval + } + + if e.maxInterval == 0 { + e.maxInterval = DefaultMaxInterval + } + + if e.multiplier == 0 { + e.multiplier = DefaultMultiplier + } +} diff --git a/yabackoff/yabackoff.go b/yabackoff/yabackoff.go new file mode 100644 index 0000000..736743a --- /dev/null +++ b/yabackoff/yabackoff.go @@ -0,0 +1,72 @@ +// Package yabackoff provides simple, self-contained back-off strategies for +// retry loops. A back-off progressively increases the time you wait between +// attempts of an operation that might fail (for example, an HTTP request). +// +// # Quick start +// +// backoff := yabackoff.NewExponential(500*time.Millisecond, 1.5, 60*time.Second) +// for { +// if err := doWork(); err == nil { +// break // success – stop retrying +// } +// backoff.Wait() // progressively longer sleeps +// } +// +// The package is dependency-free and can be safely vendored. +package yabackoff + +import ( + "time" +) + +// Default* constants are applied when the caller provides zero +// values to NewExponential, or when an Exponential is declared +// as a zero value and used without initialisation. +const ( + // DefaultInitialInterval is used when initialInterval == 0. + DefaultInitialInterval = 500 * time.Millisecond + + // DefaultMultiplier is applied when multiplier == 0. + DefaultMultiplier = 1.5 + + // DefaultMaxInterval is used when maxInterval == 0. + DefaultMaxInterval = 60 * time.Second +) + +// Backoff is the behaviour shared by all back‑off strategies in this package. +// Implementations are *not* safe for concurrent use – surround them with your +// own synchronisation if you share one instance between goroutines. +// +// Example: +// +// backoff := yabackoff.NewExponential(100*time.Millisecond, 2, time.Second) +// _ = b.Next() // 200 ms +// _ = b.Next() // 400 ms +// b.Reset() // back to 100 ms +// +// The concrete type behind the interface decides how the delays grow. +type Backoff interface { + // Next advances the strategy and returns the delay for *this* attempt. + Next() time.Duration + + // Current returns the delay that was (or will be) produced by the most + // recent (or next) call to Next(). It never mutates internal state. + Current() time.Duration + + // Wait is a convenience wrapper that simply does: + // + // time.Sleep(b.Next()) + // + // Example: + // + // backoff.Wait() // sleeps for the next back‑off interval and updates state + Wait() + + // Reset puts the strategy back to its initial state so that the very next + // call to Next() will return the initial interval again. + // + // Example: + // + // backoff.Reset() + Reset() +} diff --git a/yabackoff/yabackoff_test.go b/yabackoff/yabackoff_test.go new file mode 100644 index 0000000..14d525b --- /dev/null +++ b/yabackoff/yabackoff_test.go @@ -0,0 +1,88 @@ +package yabackoff_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yabackoff" +) + +func TestEmptySafety_Works(t *testing.T) { + exp := yabackoff.Exponential{} + got := exp.Next() + + expected := yabackoff.NewExponential( + yabackoff.DefaultInitialInterval, + yabackoff.DefaultMultiplier, + yabackoff.DefaultMaxInterval, + ) + want := expected.Next() + + assert.Equal(t, want, got) +} + +func TestNext_Works(t *testing.T) { + start := 500 * time.Millisecond + multiplier := 1.5 + maxInterval := 10 * time.Second + + backoff := yabackoff.NewExponential(start, multiplier, maxInterval) + + expected := []time.Duration{start} + + for { + last := expected[len(expected)-1] + + next := min(time.Duration(float64(last)*multiplier), maxInterval) + + expected = append(expected, next) + + if next == maxInterval { + break + } + } + + for i, want := range expected[1:] { + got := backoff.Next() + + assert.Equal(t, want, got, "mismatch at step %d", i) + } +} + +func TestReset_Works(t *testing.T) { + start := time.Second + + b := yabackoff.NewExponential(start, 2.0, 10*time.Second) + + b.Next() + b.Next() + + b.Reset() + + assert.Equal(t, start, b.Current()) +} + +func TestMaxIntervalIsRespected(t *testing.T) { + maxInterval := 5 * time.Second + + backoff := yabackoff.NewExponential(2*time.Second, 10, maxInterval) + + backoff.Next() + + assert.Equal(t, maxInterval, backoff.Current()) +} + +func TestWaitDoesSleep(t *testing.T) { + start := 100 * time.Millisecond + backoff := yabackoff.NewExponential(start, 1.0, time.Second) + + startWaiting := time.Now() + + backoff.Wait() + + elapsed := time.Since(startWaiting) + + assert.GreaterOrEqual(t, elapsed, 100*time.Millisecond) +}