Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
119 changes: 119 additions & 0 deletions yabackoff/exponential.go
Original file line number Diff line number Diff line change
@@ -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
}
}
72 changes: 72 additions & 0 deletions yabackoff/yabackoff.go
Original file line number Diff line number Diff line change
@@ -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()
}
88 changes: 88 additions & 0 deletions yabackoff/yabackoff_test.go
Original file line number Diff line number Diff line change
@@ -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)
}