first commit
This commit is contained in:
parent
a29d42f93a
commit
89eab3da05
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
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:
|
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:
|
||||||
|
|
||||||
|
|||||||
@ -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
12
backoff.go
Normal 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
71
cli/main.go
Normal 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
110
fibonacci/fibonacci.go
Normal 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
135
fibonacci/fibonacci_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
utilities/jitter.go
Normal file
24
utilities/jitter.go
Normal 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
8
utilities/math.go
Normal 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()
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user