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
44 changes: 23 additions & 21 deletions backoff.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import (
)

var DefaultBackoffPolicy = BackoffPolicy{
BaseDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
UseJitter: true,
JitterRangeMs: 300,
BaseDelay: 1 * time.Second,
MaxDelay: 30 * time.Second,
}

// Backoff defines the interface for calculating backoff delays between retries.
Expand All @@ -21,25 +19,29 @@ type Backoff interface {
Calculate(retries uint) time.Duration
}

// BackoffPolicy defines the configuration for handling retries with backoff logic.
// It provides settings for base delay, maximum delay, jitter, and the range for jitter.
// BackoffPolicy specifies the parameters for calculating exponential backoff with jitter.
// It defines the base delay and the maximum delay allowed between retries.
//
// Fields:
// - BaseDelay: Specifies the initial delay duration.
// - MaxDelay: Sets the upper bound for the backoff delay, preventing unbounded wait times.
type BackoffPolicy struct {
BaseDelay time.Duration // Base delay before the first retry (e.g., 1 second)
MaxDelay time.Duration // Maximum delay before retrying again (e.g., 60 seconds)
UseJitter bool // Whether to add random jitter to the delay
JitterRangeMs int // Maximum jitter in milliseconds (e.g., 500ms)
BaseDelay time.Duration
MaxDelay time.Duration
}

// Calculate calculates the delay before the next retry based on the backoff policy.
// It considers the retry count, base delay, maximum delay, and jitter.
// Calculate returns a jittered backoff duration based on the number of retries.
// It uses exponential backoff with full jitter, where the delay is randomly chosen
// between 0 and the calculated maximum delay.
// The maximum delay grows exponentially with each retry, capped by MaxDelay.
//
// Example:
//
// BaseDelay = 100ms, MaxDelay = 3s, retries = 3
// Max calculated delay = min(100ms * 2^3, 3s) = 800ms
// Returned delay = random duration in [0, 800ms)
func (b *BackoffPolicy) Calculate(retries uint) time.Duration {
// Calculate the exponential backoff delay, doubling with each retry.
delay := min(b.BaseDelay*time.Duration(1<<uint(retries-1)), b.MaxDelay)

if b.UseJitter && b.JitterRangeMs > 0 {
jitter := time.Duration(rand.Intn(b.JitterRangeMs)) * time.Millisecond
delay += jitter
}

return delay
maxDelay := min(b.BaseDelay*(1<<retries), b.MaxDelay)
jitter := rand.Int63n(int64(maxDelay))
return time.Duration(jitter)
}
6 changes: 2 additions & 4 deletions example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,8 @@ func main() {
}

backoffPolicy := &taskqueue.BackoffPolicy{
BaseDelay: 2 * time.Second,
MaxDelay: 60 * time.Second,
UseJitter: true,
JitterRangeMs: 500,
BaseDelay: 2 * time.Second,
MaxDelay: 60 * time.Second,
}

manager := taskqueue.NewManager(broker, taskqueue.DefaultWorkerFactory, 5, taskqueue.WithBackoffPolicy(backoffPolicy))
Expand Down
79 changes: 25 additions & 54 deletions tests/backoff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,74 +10,45 @@ import (

func TestBackoffPolicy(t *testing.T) {
tests := []struct {
name string
backoffPolicy *taskqueue.BackoffPolicy
retries uint
expectedDelay time.Duration
allowJitter bool
name string
retries uint
maxDelay time.Duration
baseDelay time.Duration
}{
{
name: "retry one",
backoffPolicy: &taskqueue.BackoffPolicy{
BaseDelay: 1 * time.Millisecond,
MaxDelay: 10 * time.Millisecond,
},
retries: 1,
expectedDelay: 1 * time.Millisecond,
name: "first retry",
retries: 1,
baseDelay: 1 * time.Millisecond,
maxDelay: 10 * time.Millisecond,
},
{
name: "retry three",
backoffPolicy: &taskqueue.BackoffPolicy{
BaseDelay: 1 * time.Millisecond,
MaxDelay: 10 * time.Millisecond,
},
retries: 3,
expectedDelay: 4 * time.Millisecond,
name: "second retry",
retries: 1,
baseDelay: 5 * time.Millisecond,
maxDelay: 20 * time.Millisecond,
},
{
name: "max delay",
backoffPolicy: &taskqueue.BackoffPolicy{
BaseDelay: 5 * time.Millisecond,
MaxDelay: 10 * time.Millisecond,
},
retries: 5,
expectedDelay: 10 * time.Millisecond, // should not exceed MaxDelay
name: "third retry",
retries: 3,
baseDelay: 5 * time.Millisecond,
maxDelay: 17 * time.Millisecond,
},
{
name: "jitter applied",
backoffPolicy: &taskqueue.BackoffPolicy{
BaseDelay: 1 * time.Millisecond,
MaxDelay: 10 * time.Millisecond,
UseJitter: true,
JitterRangeMs: 5,
},
retries: 1,
expectedDelay: 1 * time.Millisecond, // with jitter this will vary slightly
allowJitter: true,
},
{
name: "no jitter applied",
backoffPolicy: &taskqueue.BackoffPolicy{
BaseDelay: 1 * time.Millisecond,
MaxDelay: 10 * time.Millisecond,
UseJitter: false,
},
retries: 1,
expectedDelay: 1 * time.Millisecond,
allowJitter: false,
name: "fourth retry",
retries: 4,
baseDelay: 3 * time.Millisecond,
maxDelay: 9 * time.Millisecond,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
delay := tt.backoffPolicy.Calculate(tt.retries)
bp := &taskqueue.BackoffPolicy{BaseDelay: tt.baseDelay, MaxDelay: tt.maxDelay}
delay := bp.Calculate(tt.retries)
maxDelay := min(bp.BaseDelay*(1<<tt.retries), bp.MaxDelay)

if tt.allowJitter {
assert.GreaterOrEqual(t, delay, tt.expectedDelay)
assert.LessOrEqual(t, delay, tt.expectedDelay+5*time.Millisecond)
} else {
assert.Equal(t, delay, tt.expectedDelay)
}
assert.GreaterOrEqual(t, delay, time.Duration(0))
assert.Less(t, delay, maxDelay)
})
}
}
6 changes: 2 additions & 4 deletions tests/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ func TestManager(t *testing.T) {
}

backoffPolicy := &taskqueue.BackoffPolicy{
BaseDelay: 5 * time.Millisecond, // small delay for fast test
MaxDelay: 20 * time.Millisecond,
UseJitter: false,
JitterRangeMs: 0,
BaseDelay: 5 * time.Millisecond, // small delay for fast test
MaxDelay: 20 * time.Millisecond,
}

for _, tt := range tests {
Expand Down
6 changes: 2 additions & 4 deletions tests/worker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,8 @@ func TestWorkerProcesses(t *testing.T) {

t.Run("should handle failing task with backoff", func(t *testing.T) {
backoff := &taskqueue.BackoffPolicy{
BaseDelay: 5 * time.Millisecond, // small delay for fast test
MaxDelay: 20 * time.Millisecond,
UseJitter: false,
JitterRangeMs: 0,
BaseDelay: 5 * time.Millisecond, // small delay for fast test
MaxDelay: 20 * time.Millisecond,
}

mockBroker := NewMockBroker(5)
Expand Down