first commit

This commit is contained in:
William Dillon 2026-02-08 15:20:10 -05:00
parent a29d42f93a
commit 89eab3da05
9 changed files with 366 additions and 3 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 wmdillon
Copyright (c) 2026 William Dillon
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -1,2 +1,2 @@
# backoff
# package backoff
Provides a simple Backoff interface, and currently a single Backoff type (FibonacciBackoff). This can be used to control backoff between requests. It includes a simple function for adding Jitter as well. The `cli` can be built with `go build -C cli`, and can be used for sleeping by scripts.

12
backoff.go Normal file
View File

@ -0,0 +1,12 @@
package backoff
import (
"context"
"time"
)
type Backoff interface {
Reset()
Next() time.Duration
After(context.Context) <-chan time.Time
}

71
cli/main.go Normal file
View File

@ -0,0 +1,71 @@
package main
import (
"backoff/fibonacci"
"context"
"flag"
"fmt"
"math"
"os"
"strconv"
"time"
)
func FormatOutput(pause time.Duration, ns bool) string {
switch ns {
case true:
return strconv.FormatInt(pause.Nanoseconds(), 10)
default:
return pause.String()
}
}
func main() {
start := time.Now()
var (
doPanic bool
doSleep bool
multiplier time.Duration
jitter time.Duration
iteration int64
nanosecondsOutput bool
)
flag.BoolVar(&doPanic, "panic", false, "panic instead of overflowing values (default returns the max value instead of overflowing).")
flag.BoolVar(&doSleep, "sleep", false, "sleep for the resulting duration after calculating it. prints the run duration of the program instead of the calculated pause duration.")
flag.DurationVar(&multiplier, "multiplier", time.Millisecond, "pause multiplier.")
flag.DurationVar(&jitter, "jitter", 0, "jitter boundary. pause duration is randomly selected in the range of resultDuration+-jitter.")
flag.Int64Var(&iteration, "iteration", 1, "iteration to use for calculating pause time.")
flag.BoolVar(&nanosecondsOutput, "ns", false, "outputs int64 nanoseconds instead of string.")
flag.Parse()
if jitter < 0 {
jitter = time.Duration(int(math.Abs(float64(jitter.Nanoseconds()))))
}
if iteration >= int64(len(fibonacci.TheFibonacciSequence)) {
switch doPanic {
case true:
panic(fmt.Sprintf("error: input %d would overflow int64\n", iteration))
default:
maxIteration := int64(len(fibonacci.TheFibonacciSequence) - 1)
fmt.Fprintf(os.Stderr, "iteration %d would overflow int64 - using %d\n", iteration, maxIteration)
iteration = maxIteration
}
}
backoff := &fibonacci.FibonacciBackoff{
Iteration: iteration,
PauseMultiplier: multiplier,
Jitter: jitter,
}
var response string
switch doSleep {
case true:
<-backoff.After(context.Background())
response = FormatOutput(time.Since(start), nanosecondsOutput)
default:
response = FormatOutput(backoff.Next(), nanosecondsOutput)
}
fmt.Println(response)
}

110
fibonacci/fibonacci.go Normal file
View File

@ -0,0 +1,110 @@
package fibonacci
import (
"backoff/utilities"
"context"
"fmt"
"time"
)
var (
DefaultMultiplier = time.Nanosecond
)
type FibonacciBackoff struct {
Iteration int64
MaxIteration int64 // allows a lower limit than the default len(TheFibonacciSequence)
PauseMultiplier time.Duration
Jitter time.Duration
MaxPause time.Duration
foundMax int64 // stores the last successful Iteration. factors in MaxPause and MaxIteration.
}
// resets Iteration and foundMax - presumes users might
// also change the PauseMultiplier so we rediscover
// foundMax.
func (f *FibonacciBackoff) Reset() {
f.Iteration = 0
f.foundMax = 0
}
func (f *FibonacciBackoff) Next() time.Duration {
multiplier := f.PauseMultiplier
if multiplier <= 0 {
multiplier = DefaultMultiplier
}
pause := time.Duration(Fibonacci(f.Iteration))
if product, overflows := utilities.ProductWouldOverflowInt64(pause.Nanoseconds(), multiplier.Nanoseconds()); overflows {
// cowardly refusal to overflow - return time.Duration(math.MaxInt64)
// we can't calculate jitter, because that could overflow.
// realistically, this should never happen, we're talking about almost 300 years.
f.foundMax = f.Iteration - 1
f.Iteration = f.foundMax
pause = time.Duration(Fibonacci(f.Iteration)) * multiplier
} else if f.MaxPause > 0 && time.Duration(product) > f.MaxPause {
f.foundMax = f.Iteration
pause = f.MaxPause
} else {
pause = time.Duration(product)
}
// increment f.Iteration if appropriate
maxIteration := int64(len(TheFibonacciSequence) + 1)
if f.MaxIteration > 0 {
maxIteration = f.MaxIteration
}
if f.foundMax == 0 {
if f.Iteration+1 <= maxIteration {
f.Iteration++
} else {
f.foundMax = f.Iteration
}
} else {
f.Iteration = f.foundMax
}
// apply jitter if requested
if pause > 0 && f.Jitter > 0 {
pause = utilities.ApplyJitter(pause, f.Jitter)
}
return pause
}
func (f *FibonacciBackoff) After(ctx context.Context) <-chan time.Time {
pause := f.Next()
results := make(chan time.Time, 1)
go func() {
defer close(results)
select {
case t := <-time.After(pause):
results <- t
case <-ctx.Done():
results <- time.Now()
}
}()
return results
}
func Fibonacci(n int64) int64 {
switch {
case n >= int64(len(TheFibonacciSequence)):
panic(fmt.Sprintf("invalid input: %d overflows int64\n",
n))
default:
return TheFibonacciSequence[n]
}
}
var TheFibonacciSequence = [93]int64{
0, 1, 1, 2, 3, 5, 8, 13, 21, 34,
55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181,
6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229,
832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986,
102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049,
12586269025, 20365011074, 32951280099, 53316291173, 86267571272, 139583862445, 225851433717, 365435296162, 591286729879, 956722026041,
1548008755920, 2504730781961, 4052739537881, 6557470319842, 10610209857723, 17167680177565, 27777890035288, 44945570212853, 72723460248141, 117669030460994,
190392490709135, 308061521170129, 498454011879264, 806515533049393, 1304969544928657, 2111485077978050, 3416454622906707, 5527939700884757, 8944394323791464, 14472334024676221,
23416728348467685, 37889062373143906, 61305790721611591, 99194853094755497, 160500643816367088, 259695496911122585, 420196140727489673, 679891637638612258, 1100087778366101931, 1779979416004714189,
2880067194370816120, 4660046610375530309, 7540113804746346429,
}

135
fibonacci/fibonacci_test.go Normal file
View File

@ -0,0 +1,135 @@
package fibonacci
import (
"backoff/utilities"
"context"
"fmt"
"math/big"
"testing"
"time"
)
func wouldOverflow(a, b int64) (product int64, overflows bool) {
results := new(big.Int).Mul(big.NewInt(a), big.NewInt(b))
return results.Int64(), results.IsInt64()
}
func fibonacci(n int64) int64 {
switch {
case n <= 0:
return 0
case n == 1:
return 1
default:
var a, b int64 = 0, 1
for i := int64(2); i <= n; i++ {
a, b = b, a+b
}
return b
}
}
func TestFibonacciResultsArray(t *testing.T) {
for n, got := range TheFibonacciSequence {
want := fibonacci(int64(n))
if want != got {
t.Fatalf("error for input %d: wanted %d; got %d\n", n, want, got)
}
}
}
func TestFibonacciFunc(t *testing.T) {
for n := range len(TheFibonacciSequence) {
want := fibonacci(int64(n))
got := fibonacci(int64(n))
if want != got {
t.Fatalf("error for input %d: wanted %d; got %d\n", n, want, got)
}
}
}
func TestFibonacciBackoffNextDoesNotOverflow(t *testing.T) {
backoff := FibonacciBackoff{}
for n, want := range TheFibonacciSequence {
got := backoff.Next()
if time.Duration(want) != got {
t.Fatalf("error for %d: wanted %s; got %s\n", n, time.Duration(want), time.Duration(got))
}
}
backoff.Reset()
if want, got := int64(0), backoff.Iteration; want != got {
t.Fatalf("error: wanted %d; got %d after Reset()\n", want, got)
}
var previous time.Duration
backoff.PauseMultiplier = time.Second
for input, output := range TheFibonacciSequence {
var want time.Duration
switch _, overflows := utilities.ProductWouldOverflowInt64(output, backoff.PauseMultiplier.Nanoseconds()); overflows {
case true:
want = previous
default:
want = time.Duration(output) * backoff.PauseMultiplier
}
got := backoff.Next()
if want != got {
t.Fatalf("error for %d (iteration=%d); wanted %s; got %s\n", input, backoff.Iteration, want, got)
}
previous = got
}
}
func TestFibonacciBackoffIterationLimit(t *testing.T) {
backoff := FibonacciBackoff{MaxIteration: 5}
for input, output := range TheFibonacciSequence {
want := time.Duration(output)
if input >= int(backoff.MaxIteration) {
if backoff.Iteration > backoff.MaxIteration {
t.Fatalf("error: backoff.Iteration %d > max %d (%+v)\n", backoff.Iteration, backoff.MaxIteration, backoff)
}
want = time.Duration(fibonacci(5))
}
got := backoff.Next()
if want != got {
t.Fatalf("error for %d: wanted %s; got %s\n", input, want, got)
}
}
}
func TestFibonacciPauseLimit(t *testing.T) {
backoff := FibonacciBackoff{MaxPause: time.Second}
for input := range TheFibonacciSequence {
if got := backoff.Next(); got > time.Second {
t.Fatalf("error for %d: expected max %s; got %s\n", input, time.Second, got)
}
}
// test with jitter
backoff.Reset()
backoff.Jitter = time.Millisecond * 100
backoff.PauseMultiplier = time.Second
max := time.Second + backoff.Jitter
min := time.Second - backoff.Jitter
for input := range TheFibonacciSequence {
if got := backoff.Next(); input > 0 && (got > max || got < min) {
t.Fatalf("error for %d: expected value in range %s - %s; got %s\n", input, min, max, got)
}
}
fmt.Printf("finished\n")
}
func TestFibonacciBackoffAfter(t *testing.T) {
backoff := FibonacciBackoff{Iteration: 1, PauseMultiplier: time.Millisecond}
for i := 1; i <= 5; i++ {
want := time.Duration(fibonacci(int64(i))) * time.Millisecond
min, max := want-time.Millisecond*10, want+time.Millisecond*10
got := func() (took time.Duration) {
start := time.Now()
defer func() { took = time.Since(start) }()
<-backoff.After(context.Background())
return
}()
if got > max || got < min {
t.Fatalf("error: wanted value in range %s-%s; got %s\n", min, max, got)
}
}
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module backoff
go 1.25.5

24
utilities/jitter.go Normal file
View File

@ -0,0 +1,24 @@
package utilities
import (
"math"
"math/rand/v2"
"time"
)
var (
ApplyJitter = DefaultApplyJitter
)
func DefaultApplyJitter(pauseDuration time.Duration, jitter time.Duration) time.Duration {
switch {
case jitter == 0, pauseDuration == 0:
return pauseDuration
case jitter > pauseDuration:
pauseDuration, jitter = jitter, pauseDuration
}
min := pauseDuration.Nanoseconds() - jitter.Nanoseconds()
max := pauseDuration.Nanoseconds() + jitter.Nanoseconds()
rnd := int64(math.Max(float64(rand.Int64N(max-min)+min), 0))
return time.Duration(rnd)
}

8
utilities/math.go Normal file
View File

@ -0,0 +1,8 @@
package utilities
import "math/big"
func ProductWouldOverflowInt64(a, b int64) (product int64, overflows bool) {
results := new(big.Int).Mul(big.NewInt(a), big.NewInt(b))
return results.Int64(), !results.IsInt64()
}