diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bcab5e5..9d59030 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,7 +2,6 @@ # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - version: 2 updates: # Maintain dependencies for GitHub Actions @@ -17,7 +16,6 @@ updates: include: "scope" labels: - "Type: Maintenance" - # Maintain dependencies for go modules - package-ecosystem: "gomod" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5c29437..e307017 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -33,14 +33,14 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - + - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - - + - name: Autobuild uses: github/codeql-action/autobuild@v3 - - + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34235c0..3de2a88 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - + - name: Go modules hygine run: | go clean -modcache diff --git a/.gitignore b/.gitignore index e69de29..e637f31 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,2 @@ +# IDE stuff +.vscode \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 386b8b4..7675da8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,6 @@ "editor.formatOnSave": false }, "gopls": { - "ui.semanticTokens": true + "ui.semanticTokens": true, } } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 684b3d7..c588305 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,8 +43,8 @@ When submitting code, please make every effort to follow existing conventions an * All dependencies must be defined in the `go.mod` file. * Advanced IDEs and code editors (like VSCode) will take care of that, but to be sure, run `go mod tidy` to validate dependencies. -* Please run `go fmt ./...` before committing to ensure code aligns with go standards. -* We use [`golangci-lint`](https://golangci-lint.run/) for linting Go code, run `golangci-lint run --fix` before submitting PR. Editors such as Visual Studio Code or JetBrains IntelliJ; with Go support plugin will offer `golangci-lint` automatically. +* Please run `make go-fmt` before committing to ensure code aligns with go standards. +* We use [`golangci-lint`](https://golangci-lint.run/) for linting Go code, run `make go-lint` before submitting PR. Editors such as Visual Studio Code or JetBrains IntelliJ; with Go support plugin will offer `golangci-lint` automatically. * For details on the approved style, check out [Effective Go](https://golang.org/doc/effective_go.html). ### License diff --git a/backoff/backoff.go b/backoff/backoff.go index 9f5a181..fdc32a2 100644 --- a/backoff/backoff.go +++ b/backoff/backoff.go @@ -42,10 +42,8 @@ type Backoff func(minDelay, maxDelay time.Duration, attempt int) (delay time.Dur // // delay will be 8 seconds (1s * 2^3), but capped at maxDelay if exceeded. func Exponential() func(minDelay, maxDelay time.Duration, attempt int) (backoff time.Duration) { return func(minDelay, maxDelay time.Duration, attempt int) (backoff time.Duration) { - // Calculate the exponential backoff delay based on the attempt number. backoff = time.Duration(math.Pow(2, float64(attempt)) * float64(minDelay)) - // Cap the delay at the maximum value. if backoff > maxDelay { backoff = maxDelay } @@ -77,18 +75,14 @@ func ExponentialWithEqualJitter() func(minDelay, maxDelay time.Duration, attempt mutex := &sync.Mutex{} return func(minDelay, maxDelay time.Duration, attempt int) (backoff time.Duration) { - // Calculate the base exponential backoff delay. backoff = time.Duration(math.Pow(2, float64(attempt)) * float64(minDelay)) - // Lock the mutex to ensure thread-safe jitter calculation. mutex.Lock() jittered := jitter.Equal(backoff) mutex.Unlock() - // Add the jitter to the base backoff. backoff += jittered - // Cap the delay at the maximum value. if backoff > maxDelay { backoff = maxDelay } @@ -120,18 +114,14 @@ func ExponentialWithFullJitter() func(minDelay, maxDelay time.Duration, attempt mutex := &sync.Mutex{} return func(minDelay, maxDelay time.Duration, attempt int) (backoff time.Duration) { - // Calculate the base exponential backoff delay. backoff = time.Duration(math.Pow(2, float64(attempt)) * float64(minDelay)) - // Lock the mutex to ensure thread-safe jitter calculation. mutex.Lock() jittered := jitter.Full(backoff) mutex.Unlock() - // Add the jitter to the base backoff. backoff += jittered - // Cap the delay at the maximum value. if backoff > maxDelay { backoff = maxDelay } @@ -163,21 +153,16 @@ func ExponentialWithDecorrelatedJitter() func(minDelay, maxDelay time.Duration, mutex := &sync.Mutex{} return func(minDelay, maxDelay time.Duration, attempt int) (backoff time.Duration) { - // Calculate the previous backoff delay based on the previous attempt number. previous := time.Duration(math.Pow(2, float64(attempt-1)) * float64(minDelay)) - // Calculate the base exponential backoff delay for the current attempt. backoff = time.Duration(math.Pow(2, float64(attempt)) * float64(minDelay)) - // Lock the mutex to ensure thread-safe jitter calculation. mutex.Lock() jittered := jitter.Decorrelated(minDelay, maxDelay, previous) mutex.Unlock() - // Add the decorrelated jitter to the base backoff. backoff += jittered - // Cap the delay at the maximum value. if backoff > maxDelay { backoff = maxDelay } diff --git a/backoff/backoff_test.go b/backoff/backoff_test.go index e32be22..a1367d8 100644 --- a/backoff/backoff_test.go +++ b/backoff/backoff_test.go @@ -8,7 +8,6 @@ import ( "github.com/stretchr/testify/assert" ) -// TestExponentialBackoff verifies that the Exponential backoff function returns correct delays. func TestExponentialBackoff(t *testing.T) { t.Parallel() @@ -28,11 +27,11 @@ func TestExponentialBackoff(t *testing.T) { for _, tt := range tests { delay := b(tt.minDelay, tt.maxDelay, tt.attempt) + assert.Equal(t, tt.expected, delay, "Unexpected backoff duration for attempt %d", tt.attempt) } } -// TestExponentialWithEqualJitterBackoff verifies that ExponentialWithEqualJitter returns correct delays with jitter. func TestExponentialWithEqualJitterBackoff(t *testing.T) { t.Parallel() @@ -50,12 +49,12 @@ func TestExponentialWithEqualJitterBackoff(t *testing.T) { for _, tt := range tests { delay := b(tt.minDelay, tt.maxDelay, tt.attempt) + assert.GreaterOrEqual(t, delay, tt.minDelay, "Backoff delay should not be less than the minimum") assert.LessOrEqual(t, delay, tt.maxDelay, "Backoff delay should not exceed the maximum") } } -// TestExponentialWithFullJitterBackoff verifies that ExponentialWithFullJitter returns correct delays with full jitter. func TestExponentialWithFullJitterBackoff(t *testing.T) { t.Parallel() @@ -73,12 +72,12 @@ func TestExponentialWithFullJitterBackoff(t *testing.T) { for _, tt := range tests { delay := b(tt.minDelay, tt.maxDelay, tt.attempt) + assert.GreaterOrEqual(t, delay, tt.minDelay, "Backoff delay should not be less than the minimum") assert.LessOrEqual(t, delay, tt.maxDelay, "Backoff delay should not exceed the maximum") } } -// TestExponentialWithDecorrelatedJitterBackoff verifies that ExponentialWithDecorrelatedJitter returns correct delays with decorrelated jitter. func TestExponentialWithDecorrelatedJitterBackoff(t *testing.T) { t.Parallel() @@ -96,6 +95,7 @@ func TestExponentialWithDecorrelatedJitterBackoff(t *testing.T) { for _, tt := range tests { delay := b(tt.minDelay, tt.maxDelay, tt.attempt) + assert.GreaterOrEqual(t, delay, tt.minDelay, "Backoff delay should not be less than the minimum") assert.LessOrEqual(t, delay, tt.maxDelay, "Backoff delay should not exceed the maximum") } diff --git a/jitter/jitter.go b/jitter/jitter.go index c4390ae..51f62b6 100644 --- a/jitter/jitter.go +++ b/jitter/jitter.go @@ -32,10 +32,8 @@ import ( // jitteredBackoff := jitter.Equal(backoff) // // jitteredBackoff will be somewhere between 5 seconds and 10 seconds. func Equal(backoff time.Duration) (jitter time.Duration) { - // Calculate the midpoint of the backoff duration. midpoint := backoff / 2 - // Add a random duration between 0 and the midpoint to the midpoint. jitter = midpoint + getRandomDuration(midpoint) return @@ -63,7 +61,6 @@ func Equal(backoff time.Duration) (jitter time.Duration) { // jitteredBackoff := jitter.Full(backoff) // // jitteredBackoff will be somewhere between 0 and 10 seconds. func Full(backoff time.Duration) (jitter time.Duration) { - // Generate a random duration between 0 and the backoff duration. jitter = getRandomDuration(backoff) return @@ -99,16 +96,14 @@ func Full(backoff time.Duration) (jitter time.Duration) { // // jitteredBackoff will be somewhere between minDelay and maxDelay, // // bounded by the previous backoff value. func Decorrelated(minDelay, maxDelay, previous time.Duration) (jitter time.Duration) { - // If this is the first call, use the minimum duration as the previous value. if previous == 0 { previous = minDelay } - // Generate a random duration within the range [minDelay, previous*3]. jitter = getRandomDuration(previous * 3) + jitter += minDelay - // Ensure that the jitter does not exceed the maximum duration. if jitter > maxDelay { jitter = maxDelay } @@ -139,19 +134,15 @@ func Decorrelated(minDelay, maxDelay, previous time.Duration) (jitter time.Durat // randomDuration := getRandomDuration(10 * time.Second) // // randomDuration will be a random time.Duration between 0 and 10 seconds. func getRandomDuration(maxDuration time.Duration) (duration time.Duration) { - // Return 0 if the maxDurationimum value is invalid or non-positive. if maxDuration <= 0 { return 0 } - // Generate a cryptographically secure random integer between 0 and maxDuration. n, err := rand.Int(rand.Reader, big.NewInt(int64(maxDuration))) if err != nil { - // If an error occurs during random number generation, return the maxDurationimum value. return maxDuration } - // Convert the random integer to a time.Duration and return it. duration = time.Duration(n.Int64()) return diff --git a/jitter/jitter_test.go b/jitter/jitter_test.go index 1732169..f824ad3 100644 --- a/jitter/jitter_test.go +++ b/jitter/jitter_test.go @@ -13,12 +13,10 @@ func TestEqualJitter(t *testing.T) { backoff := 10 * time.Second - // Run the Equal jitter function multiple times to ensure randomness. for range 100 { jittered := jitter.Equal(backoff) midpoint := backoff / 2 - // Check that jittered value is within the expected range. assert.GreaterOrEqual(t, jittered, midpoint, "Jittered duration should be at least the midpoint") assert.Less(t, jittered, backoff, "Jittered duration should be less than the original backoff") } @@ -32,7 +30,6 @@ func TestEqualJitter_MidpointLogic(t *testing.T) { jittered := jitter.Equal(backoff) - // Check that the jitter is at least the midpoint. assert.GreaterOrEqual(t, jittered, expectedMidpoint, "Jittered duration should be at least the midpoint") } @@ -43,7 +40,6 @@ func TestEqualJitter_ZeroDuration(t *testing.T) { jittered := jitter.Equal(backoff) - // Check that when the backoff is 0, the jittered value should also be 0. assert.Equal(t, 0*time.Second, jittered, "Jittered duration should be 0 when the backoff is 0") } @@ -52,11 +48,9 @@ func TestFullJitter(t *testing.T) { backoff := 10 * time.Second - // Run the Full jitter function multiple times to ensure randomness. for range 100 { jittered := jitter.Full(backoff) - // Check that jittered value is within the expected range. assert.GreaterOrEqual(t, jittered, 0*time.Second, "Jittered duration should be at least 0") assert.Less(t, jittered, backoff, "Jittered duration should be less than the original backoff") } @@ -69,7 +63,6 @@ func TestFullJitter_ZeroDuration(t *testing.T) { jittered := jitter.Full(backoff) - // Check that when the backoff is 0, the jittered value should also be 0. assert.Equal(t, 0*time.Second, jittered, "Jittered duration should be 0 when the backoff is 0") } @@ -82,7 +75,6 @@ func TestDecorrelatedJitter_FirstCall(t *testing.T) { jittered := jitter.Decorrelated(minDelay, maxDelay, previous) - // Check that jittered value is within the range [minDelay, maxDelay]. assert.GreaterOrEqual(t, jittered, minDelay, "Jittered duration should be at least the minimum") assert.LessOrEqual(t, jittered, maxDelay, "Jittered duration should not exceed the maximum") } @@ -94,11 +86,9 @@ func TestDecorrelatedJitter_SubsequentCalls(t *testing.T) { maxDelay := 10 * time.Second previous := 4 * time.Second - // Run the Decorrelated jitter function multiple times to ensure randomness. for range 100 { jittered := jitter.Decorrelated(minDelay, maxDelay, previous) - // Ensure jittered value is within the expected range. assert.GreaterOrEqual(t, jittered, minDelay, "Jittered duration should be at least the minimum") assert.LessOrEqual(t, jittered, maxDelay, "Jittered duration should not exceed the maximum") assert.LessOrEqual(t, jittered, previous*3, "Jittered duration should not exceed three times the previous duration") @@ -114,7 +104,6 @@ func TestDecorrelatedJitter_MaxBoundary(t *testing.T) { jittered := jitter.Decorrelated(minDelay, maxDelay, previous) - // Ensure that the jittered value is within the expected range and does not exceed the maximum. assert.GreaterOrEqual(t, jittered, minDelay, "Jittered duration should be at least the minimum") assert.LessOrEqual(t, jittered, maxDelay, "Jittered duration should not exceed the maximum") }