From e8061b89bad270adea8c30f5d943b6a6a524d804 Mon Sep 17 00:00:00 2001 From: Panot Wongkhot Date: Thu, 5 Jun 2025 21:51:29 +0700 Subject: [PATCH 1/9] add backoff --- backoff.go | 144 ++++++++++++++++++++++++++++++++++ backoff_test.go | 204 ++++++++++++++++++++++++++++++++++++++++++++++++ tx.go | 53 ++++++++++++- 3 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 backoff.go create mode 100644 backoff_test.go diff --git a/backoff.go b/backoff.go new file mode 100644 index 0000000..98d5bc1 --- /dev/null +++ b/backoff.go @@ -0,0 +1,144 @@ +package pgsql + +import ( + "math" + "math/rand/v2" + "time" +) + +// BackoffConfig contains common configuration for all backoff strategies +type BackoffConfig struct { + BaseDelay time.Duration // Base delay for backoff + MaxDelay time.Duration // Maximum delay cap +} + +// ExponentialBackoffConfig contains configuration for exponential backoff +type ExponentialBackoffConfig struct { + BackoffConfig + Multiplier float64 // Multiplier for exponential growth +} + +// LinearBackoffConfig contains configuration for linear backoff +type LinearBackoffConfig struct { + BackoffConfig + Increment time.Duration // Amount to increase delay each attempt +} + +// JitterType defines the type of jitter to apply +type JitterType int + +const ( + // NoJitter applies no jitter + NoJitter JitterType = iota + // FullJitter applies full jitter (0 to calculated delay) + FullJitter + // EqualJitter applies equal jitter (half fixed + half random) + EqualJitter +) + +// ExponentialBackoffWithJitterConfig contains configuration for exponential backoff with jitter +type ExponentialBackoffWithJitterConfig struct { + ExponentialBackoffConfig + JitterType JitterType +} + +// NewExponentialBackoff creates a new exponential backoff function +func NewExponentialBackoff(config ExponentialBackoffConfig) BackoffDelayFunc { + return func(attempt int) time.Duration { + delay := time.Duration(float64(config.BaseDelay) * math.Pow(config.Multiplier, float64(attempt))) + if delay > config.MaxDelay { + delay = config.MaxDelay + } + return delay + } +} + +// NewExponentialBackoffWithJitter creates a new exponential backoff function with jitter +func NewExponentialBackoffWithJitter(config ExponentialBackoffWithJitterConfig) BackoffDelayFunc { + return func(attempt int) time.Duration { + baseDelay := time.Duration(float64(config.BaseDelay) * math.Pow(config.Multiplier, float64(attempt))) + if baseDelay > config.MaxDelay { + baseDelay = config.MaxDelay + } + + var delay time.Duration + switch config.JitterType { + case FullJitter: + // Full jitter: random delay between 0 and calculated delay + if baseDelay > 0 { + delay = time.Duration(rand.Int64N(int64(baseDelay))) + } else { + delay = baseDelay + } + case EqualJitter: + // Equal jitter: half fixed + half random + half := baseDelay / 2 + if half > 0 { + delay = half + time.Duration(rand.Int64N(int64(half))) + } else { + delay = baseDelay + } + default: + delay = baseDelay + } + + return delay + } +} + +// NewLinearBackoff creates a new linear backoff function +func NewLinearBackoff(config LinearBackoffConfig) BackoffDelayFunc { + return func(attempt int) time.Duration { + delay := config.BaseDelay + time.Duration(attempt)*config.Increment + if delay > config.MaxDelay { + delay = config.MaxDelay + } + return delay + } +} + +func DefaultExponentialBackoff() BackoffDelayFunc { + return NewExponentialBackoff(ExponentialBackoffConfig{ + BackoffConfig: BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 5 * time.Second, + }, + Multiplier: 2.0, + }) +} + +func DefaultExponentialBackoffWithFullJitter() BackoffDelayFunc { + return NewExponentialBackoffWithJitter(ExponentialBackoffWithJitterConfig{ + ExponentialBackoffConfig: ExponentialBackoffConfig{ + BackoffConfig: BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 5 * time.Second, + }, + Multiplier: 2.0, + }, + JitterType: FullJitter, + }) +} + +func DefaultExponentialBackoffWithEqualJitter() BackoffDelayFunc { + return NewExponentialBackoffWithJitter(ExponentialBackoffWithJitterConfig{ + ExponentialBackoffConfig: ExponentialBackoffConfig{ + BackoffConfig: BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 5 * time.Second, + }, + Multiplier: 2.0, + }, + JitterType: EqualJitter, + }) +} + +func DefaultLinearBackoff() BackoffDelayFunc { + return NewLinearBackoff(LinearBackoffConfig{ + BackoffConfig: BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 5 * time.Second, + }, + Increment: 100 * time.Millisecond, + }) +} diff --git a/backoff_test.go b/backoff_test.go new file mode 100644 index 0000000..02076ab --- /dev/null +++ b/backoff_test.go @@ -0,0 +1,204 @@ +package pgsql_test + +import ( + "fmt" + "testing" + "time" + + "github.com/acoshift/pgsql" +) + +func TestExponentialBackoff(t *testing.T) { + t.Parallel() + + config := pgsql.ExponentialBackoffConfig{ + BackoffConfig: pgsql.BackoffConfig{ + BaseDelay: 10 * time.Millisecond, + MaxDelay: 1 * time.Second, + }, + Multiplier: 2.0, + } + backoff := pgsql.NewExponentialBackoff(config) + + // Test exponential growth + delays := []time.Duration{} + for i := 0; i < 5; i++ { + delay := backoff(i) + delays = append(delays, delay) + } + + // Verify exponential growth + for i := 1; i < len(delays); i++ { + if delays[i] < delays[i-1] { + t.Errorf("Expected delay[%d] >= delay[%d], got %v < %v", i, i-1, delays[i], delays[i-1]) + } + } +} + +func TestExponentialBackoffWithFullJitter(t *testing.T) { + t.Parallel() + + config := pgsql.ExponentialBackoffWithJitterConfig{ + ExponentialBackoffConfig: pgsql.ExponentialBackoffConfig{ + BackoffConfig: pgsql.BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 1 * time.Second, + }, + Multiplier: 2.0, + }, + JitterType: pgsql.FullJitter, + } + backoff := pgsql.NewExponentialBackoffWithJitter(config) + + // Test that jitter introduces randomness + var delays []time.Duration + for i := 0; i < 10; i++ { + delay := backoff(3) // Use same attempt number + delays = append(delays, delay) + } + + // Check that not all delays are the same (indicating jitter is working) + allSame := true + for i := 1; i < len(delays); i++ { + if delays[i] != delays[0] { + allSame = false + break + } + } + if allSame { + t.Error("Expected jitter to produce different delays, but all delays were the same") + } +} + +func TestExponentialBackoffWithEqualJitter(t *testing.T) { + t.Parallel() + + config := pgsql.ExponentialBackoffWithJitterConfig{ + ExponentialBackoffConfig: pgsql.ExponentialBackoffConfig{ + BackoffConfig: pgsql.BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 1 * time.Second, + }, + Multiplier: 2.0, + }, + JitterType: pgsql.EqualJitter, + } + backoff := pgsql.NewExponentialBackoffWithJitter(config) + + delay := backoff(2) + + // With equal jitter, delay should be at least half of the calculated delay + expectedMin := 200 * time.Millisecond // (100ms * 2^2) / 2 = 200ms + if delay < expectedMin { + t.Errorf("Expected delay >= %v with equal jitter, got %v", expectedMin, delay) + } +} + +func TestLinearBackoff(t *testing.T) { + t.Parallel() + + config := pgsql.LinearBackoffConfig{ + BackoffConfig: pgsql.BackoffConfig{ + BaseDelay: 50 * time.Millisecond, + MaxDelay: 500 * time.Millisecond, + }, + Increment: 50 * time.Millisecond, + } + backoff := pgsql.NewLinearBackoff(config) + + // Test linear growth + delays := []time.Duration{} + for i := 0; i < 5; i++ { + delay := backoff(i) + delays = append(delays, delay) + } + + // Verify linear growth + for i := 1; i < len(delays); i++ { + expectedIncrease := 50 * time.Millisecond + actualIncrease := delays[i] - delays[i-1] + + if actualIncrease != expectedIncrease { + t.Errorf("Expected linear increase of %v, got %v", expectedIncrease, actualIncrease) + } + } +} + +func TestDefaultBackoffFunctions(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + backoff pgsql.BackoffDelayFunc + }{ + {"DefaultExponentialBackoff", pgsql.DefaultExponentialBackoff()}, + {"DefaultExponentialBackoffWithFullJitter", pgsql.DefaultExponentialBackoffWithFullJitter()}, + {"DefaultExponentialBackoffWithEqualJitter", pgsql.DefaultExponentialBackoffWithEqualJitter()}, + {"DefaultLinearBackoff", pgsql.DefaultLinearBackoff()}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + delay := tc.backoff(0) + + // Should have some delay (base delay should be at least 50ms for all defaults) + if delay < 50*time.Millisecond { + t.Errorf("Expected some delay, got %v", delay) + } + }) + } +} + +func TestMaxDelayIsRespected(t *testing.T) { + t.Parallel() + + config := pgsql.ExponentialBackoffConfig{ + BackoffConfig: pgsql.BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 200 * time.Millisecond, // Very low max delay + }, + Multiplier: 2.0, + } + backoff := pgsql.NewExponentialBackoff(config) + + // Test that max delay is respected even with high attempt numbers + delay := backoff(10) // This would normally result in a very long delay + + maxExpected := 200 * time.Millisecond + + if delay > maxExpected { + t.Errorf("Expected delay capped at %v, got %v", maxExpected, delay) + } +} + +// Example demonstrating usage with transaction retry +func ExampleNewExponentialBackoff() { + // Create a custom exponential backoff + backoff := pgsql.NewExponentialBackoff(pgsql.ExponentialBackoffConfig{ + BackoffConfig: pgsql.BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 5 * time.Second, + }, + Multiplier: 2.0, + }) + + // Use with transaction options + opts := &pgsql.TxOptions{ + MaxAttempts: 5, + BackoffDelayFunc: backoff, + } + + fmt.Printf("Transaction options configured with custom backoff (MaxAttempts: %d)\n", opts.MaxAttempts) + // Output: Transaction options configured with custom backoff (MaxAttempts: 5) +} + +func ExampleDefaultExponentialBackoffWithFullJitter() { + // Use a pre-configured exponential backoff with full jitter + opts := &pgsql.TxOptions{ + MaxAttempts: 3, + BackoffDelayFunc: pgsql.DefaultExponentialBackoffWithFullJitter(), + } + + fmt.Printf("Transaction options with full jitter backoff (MaxAttempts: %d)\n", opts.MaxAttempts) + // Output: Transaction options with full jitter backoff (MaxAttempts: 3) +} diff --git a/tx.go b/tx.go index e7615ef..70e857c 100644 --- a/tx.go +++ b/tx.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "time" ) // ErrAbortTx rollbacks transaction and return nil error @@ -14,10 +15,14 @@ type BeginTxer interface { BeginTx(context.Context, *sql.TxOptions) (*sql.Tx, error) } +// BackoffDelayFunc is a function type that defines the delay for backoff +type BackoffDelayFunc func(attempt int) time.Duration + // TxOptions is the transaction options type TxOptions struct { sql.TxOptions - MaxAttempts int + MaxAttempts int + BackoffDelayFunc BackoffDelayFunc } const ( @@ -54,6 +59,14 @@ func RunInTxContext(ctx context.Context, db BeginTxer, opts *TxOptions, fn func( if opts.Isolation == sql.LevelDefault { option.Isolation = sql.LevelSerializable } + + option.BackoffDelayFunc = opts.BackoffDelayFunc + } + + var txBackoffTimer *backoffTimer + if option.BackoffDelayFunc != nil { + txBackoffTimer = newTxBackoffTimer(option.BackoffDelayFunc) + defer txBackoffTimer.Stop() } f := func() error { @@ -80,7 +93,45 @@ func RunInTxContext(ctx context.Context, db BeginTxer, opts *TxOptions, fn func( if !IsSerializationFailure(err) { return err } + + if txBackoffTimer != nil && i < option.MaxAttempts-1 { + if err = txBackoffTimer.Wait(ctx, i); err != nil { + return err + } + } } return err } + +type backoffTimer struct { + timer *time.Timer + backOffDelayFunc BackoffDelayFunc +} + +func newTxBackoffTimer(backoffDelayFunc BackoffDelayFunc) *backoffTimer { + return &backoffTimer{ + timer: time.NewTimer(0), + backOffDelayFunc: backoffDelayFunc, + } +} + +func (b *backoffTimer) Wait(ctx context.Context, attempt int) error { + delay := b.backOffDelayFunc(attempt) + if delay <= 0 { + return nil + } + + b.timer.Reset(delay) + + select { + case <-ctx.Done(): + return ctx.Err() + case <-b.timer.C: + return nil + } +} + +func (b *backoffTimer) Stop() bool { + return b.timer.Stop() +} From 7d24978db2367b5e8987515f7fc26e56b58d235c Mon Sep 17 00:00:00 2001 From: Panot Wongkhot Date: Thu, 5 Jun 2025 22:19:08 +0700 Subject: [PATCH 2/9] ci: update go version --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c08eb0b..119fba2 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,7 +19,7 @@ jobs: - 5432:5432 strategy: matrix: - go: ['1.20', '1.21'] + go: ['1.22', '1.23', '1.24'] name: Go ${{ matrix.go }} steps: - uses: actions/checkout@v3 From 64870c0b004afcd803e6ea5d0a72cbcab8fd19c8 Mon Sep 17 00:00:00 2001 From: Panot Wongkhot Date: Thu, 5 Jun 2025 22:52:45 +0700 Subject: [PATCH 3/9] update backoff test --- backoff_test.go | 118 ++++++++++++++---------------------------------- 1 file changed, 35 insertions(+), 83 deletions(-) diff --git a/backoff_test.go b/backoff_test.go index 02076ab..025f35a 100644 --- a/backoff_test.go +++ b/backoff_test.go @@ -1,7 +1,6 @@ package pgsql_test import ( - "fmt" "testing" "time" @@ -22,7 +21,7 @@ func TestExponentialBackoff(t *testing.T) { // Test exponential growth delays := []time.Duration{} - for i := 0; i < 5; i++ { + for i := 0; i < 10; i++ { delay := backoff(i) delays = append(delays, delay) } @@ -33,6 +32,14 @@ func TestExponentialBackoff(t *testing.T) { t.Errorf("Expected delay[%d] >= delay[%d], got %v < %v", i, i-1, delays[i], delays[i-1]) } } + + // Verify max delay + for i := 0; i < 10; i++ { + delay := backoff(i) + if delay > config.MaxDelay { + t.Errorf("Expected delay[%d] <= MaxDelay (%v), got %v", i, config.MaxDelay, delay) + } + } } func TestExponentialBackoffWithFullJitter(t *testing.T) { @@ -68,6 +75,14 @@ func TestExponentialBackoffWithFullJitter(t *testing.T) { if allSame { t.Error("Expected jitter to produce different delays, but all delays were the same") } + + // Verify max delay + for i := 0; i < 15; i++ { + delay := backoff(i) + if delay > config.MaxDelay { + t.Errorf("Expected delay[%d] <= MaxDelay (%v), got %v", i, config.MaxDelay, delay) + } + } } func TestExponentialBackoffWithEqualJitter(t *testing.T) { @@ -92,6 +107,14 @@ func TestExponentialBackoffWithEqualJitter(t *testing.T) { if delay < expectedMin { t.Errorf("Expected delay >= %v with equal jitter, got %v", expectedMin, delay) } + + // Verify max delay + for i := 0; i < 15; i++ { + delay := backoff(i) + if delay > config.MaxDelay { + t.Errorf("Expected delay[%d] <= MaxDelay (%v), got %v", i, config.MaxDelay, delay) + } + } } func TestLinearBackoff(t *testing.T) { @@ -99,10 +122,10 @@ func TestLinearBackoff(t *testing.T) { config := pgsql.LinearBackoffConfig{ BackoffConfig: pgsql.BackoffConfig{ - BaseDelay: 50 * time.Millisecond, - MaxDelay: 500 * time.Millisecond, + BaseDelay: 100 * time.Millisecond, + MaxDelay: 1 * time.Second, }, - Increment: 50 * time.Millisecond, + Increment: 100 * time.Millisecond, } backoff := pgsql.NewLinearBackoff(config) @@ -115,90 +138,19 @@ func TestLinearBackoff(t *testing.T) { // Verify linear growth for i := 1; i < len(delays); i++ { - expectedIncrease := 50 * time.Millisecond + expectedIncrease := 100 * time.Millisecond actualIncrease := delays[i] - delays[i-1] if actualIncrease != expectedIncrease { t.Errorf("Expected linear increase of %v, got %v", expectedIncrease, actualIncrease) } } -} -func TestDefaultBackoffFunctions(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - backoff pgsql.BackoffDelayFunc - }{ - {"DefaultExponentialBackoff", pgsql.DefaultExponentialBackoff()}, - {"DefaultExponentialBackoffWithFullJitter", pgsql.DefaultExponentialBackoffWithFullJitter()}, - {"DefaultExponentialBackoffWithEqualJitter", pgsql.DefaultExponentialBackoffWithEqualJitter()}, - {"DefaultLinearBackoff", pgsql.DefaultLinearBackoff()}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - delay := tc.backoff(0) - - // Should have some delay (base delay should be at least 50ms for all defaults) - if delay < 50*time.Millisecond { - t.Errorf("Expected some delay, got %v", delay) - } - }) - } -} - -func TestMaxDelayIsRespected(t *testing.T) { - t.Parallel() - - config := pgsql.ExponentialBackoffConfig{ - BackoffConfig: pgsql.BackoffConfig{ - BaseDelay: 100 * time.Millisecond, - MaxDelay: 200 * time.Millisecond, // Very low max delay - }, - Multiplier: 2.0, - } - backoff := pgsql.NewExponentialBackoff(config) - - // Test that max delay is respected even with high attempt numbers - delay := backoff(10) // This would normally result in a very long delay - - maxExpected := 200 * time.Millisecond - - if delay > maxExpected { - t.Errorf("Expected delay capped at %v, got %v", maxExpected, delay) - } -} - -// Example demonstrating usage with transaction retry -func ExampleNewExponentialBackoff() { - // Create a custom exponential backoff - backoff := pgsql.NewExponentialBackoff(pgsql.ExponentialBackoffConfig{ - BackoffConfig: pgsql.BackoffConfig{ - BaseDelay: 100 * time.Millisecond, - MaxDelay: 5 * time.Second, - }, - Multiplier: 2.0, - }) - - // Use with transaction options - opts := &pgsql.TxOptions{ - MaxAttempts: 5, - BackoffDelayFunc: backoff, - } - - fmt.Printf("Transaction options configured with custom backoff (MaxAttempts: %d)\n", opts.MaxAttempts) - // Output: Transaction options configured with custom backoff (MaxAttempts: 5) -} - -func ExampleDefaultExponentialBackoffWithFullJitter() { - // Use a pre-configured exponential backoff with full jitter - opts := &pgsql.TxOptions{ - MaxAttempts: 3, - BackoffDelayFunc: pgsql.DefaultExponentialBackoffWithFullJitter(), + // Verify max delay + for i := 0; i < 15; i++ { + delay := backoff(i) + if delay > config.MaxDelay { + t.Errorf("Expected delay[%d] <= MaxDelay (%v), got %v", i, config.MaxDelay, delay) + } } - - fmt.Printf("Transaction options with full jitter backoff (MaxAttempts: %d)\n", opts.MaxAttempts) - // Output: Transaction options with full jitter backoff (MaxAttempts: 3) } From 16312921b055d755dee2d139d3f587b7c34a1419 Mon Sep 17 00:00:00 2001 From: Panot Wongkhot Date: Thu, 5 Jun 2025 23:29:01 +0700 Subject: [PATCH 4/9] refine --- backoff.go | 43 ++++++++++++------------------------------- backoff_test.go | 28 ++++++++++++---------------- 2 files changed, 24 insertions(+), 47 deletions(-) diff --git a/backoff.go b/backoff.go index 98d5bc1..16a8b28 100644 --- a/backoff.go +++ b/backoff.go @@ -16,6 +16,7 @@ type BackoffConfig struct { type ExponentialBackoffConfig struct { BackoffConfig Multiplier float64 // Multiplier for exponential growth + JitterType JitterType } // LinearBackoffConfig contains configuration for linear backoff @@ -36,25 +37,8 @@ const ( EqualJitter ) -// ExponentialBackoffWithJitterConfig contains configuration for exponential backoff with jitter -type ExponentialBackoffWithJitterConfig struct { - ExponentialBackoffConfig - JitterType JitterType -} - // NewExponentialBackoff creates a new exponential backoff function func NewExponentialBackoff(config ExponentialBackoffConfig) BackoffDelayFunc { - return func(attempt int) time.Duration { - delay := time.Duration(float64(config.BaseDelay) * math.Pow(config.Multiplier, float64(attempt))) - if delay > config.MaxDelay { - delay = config.MaxDelay - } - return delay - } -} - -// NewExponentialBackoffWithJitter creates a new exponential backoff function with jitter -func NewExponentialBackoffWithJitter(config ExponentialBackoffWithJitterConfig) BackoffDelayFunc { return func(attempt int) time.Duration { baseDelay := time.Duration(float64(config.BaseDelay) * math.Pow(config.Multiplier, float64(attempt))) if baseDelay > config.MaxDelay { @@ -104,31 +88,28 @@ func DefaultExponentialBackoff() BackoffDelayFunc { MaxDelay: 5 * time.Second, }, Multiplier: 2.0, + JitterType: NoJitter, }) } func DefaultExponentialBackoffWithFullJitter() BackoffDelayFunc { - return NewExponentialBackoffWithJitter(ExponentialBackoffWithJitterConfig{ - ExponentialBackoffConfig: ExponentialBackoffConfig{ - BackoffConfig: BackoffConfig{ - BaseDelay: 100 * time.Millisecond, - MaxDelay: 5 * time.Second, - }, - Multiplier: 2.0, + return NewExponentialBackoff(ExponentialBackoffConfig{ + BackoffConfig: BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 5 * time.Second, }, + Multiplier: 2.0, JitterType: FullJitter, }) } func DefaultExponentialBackoffWithEqualJitter() BackoffDelayFunc { - return NewExponentialBackoffWithJitter(ExponentialBackoffWithJitterConfig{ - ExponentialBackoffConfig: ExponentialBackoffConfig{ - BackoffConfig: BackoffConfig{ - BaseDelay: 100 * time.Millisecond, - MaxDelay: 5 * time.Second, - }, - Multiplier: 2.0, + return NewExponentialBackoff(ExponentialBackoffConfig{ + BackoffConfig: BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 5 * time.Second, }, + Multiplier: 2.0, JitterType: EqualJitter, }) } diff --git a/backoff_test.go b/backoff_test.go index 025f35a..577d1e3 100644 --- a/backoff_test.go +++ b/backoff_test.go @@ -45,17 +45,15 @@ func TestExponentialBackoff(t *testing.T) { func TestExponentialBackoffWithFullJitter(t *testing.T) { t.Parallel() - config := pgsql.ExponentialBackoffWithJitterConfig{ - ExponentialBackoffConfig: pgsql.ExponentialBackoffConfig{ - BackoffConfig: pgsql.BackoffConfig{ - BaseDelay: 100 * time.Millisecond, - MaxDelay: 1 * time.Second, - }, - Multiplier: 2.0, + config := pgsql.ExponentialBackoffConfig{ + BackoffConfig: pgsql.BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 1 * time.Second, }, + Multiplier: 2.0, JitterType: pgsql.FullJitter, } - backoff := pgsql.NewExponentialBackoffWithJitter(config) + backoff := pgsql.NewExponentialBackoff(config) // Test that jitter introduces randomness var delays []time.Duration @@ -88,17 +86,15 @@ func TestExponentialBackoffWithFullJitter(t *testing.T) { func TestExponentialBackoffWithEqualJitter(t *testing.T) { t.Parallel() - config := pgsql.ExponentialBackoffWithJitterConfig{ - ExponentialBackoffConfig: pgsql.ExponentialBackoffConfig{ - BackoffConfig: pgsql.BackoffConfig{ - BaseDelay: 100 * time.Millisecond, - MaxDelay: 1 * time.Second, - }, - Multiplier: 2.0, + config := pgsql.ExponentialBackoffConfig{ + BackoffConfig: pgsql.BackoffConfig{ + BaseDelay: 100 * time.Millisecond, + MaxDelay: 1 * time.Second, }, + Multiplier: 2.0, JitterType: pgsql.EqualJitter, } - backoff := pgsql.NewExponentialBackoffWithJitter(config) + backoff := pgsql.NewExponentialBackoff(config) delay := backoff(2) From 40714673c0a008f9b2bb065b21609944e253cbab Mon Sep 17 00:00:00 2001 From: Panot Wongkhot Date: Thu, 5 Jun 2025 23:30:28 +0700 Subject: [PATCH 5/9] change wording --- tx.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tx.go b/tx.go index 70e857c..200678d 100644 --- a/tx.go +++ b/tx.go @@ -63,10 +63,10 @@ func RunInTxContext(ctx context.Context, db BeginTxer, opts *TxOptions, fn func( option.BackoffDelayFunc = opts.BackoffDelayFunc } - var txBackoffTimer *backoffTimer + var backoffTimer *backoffTimer if option.BackoffDelayFunc != nil { - txBackoffTimer = newTxBackoffTimer(option.BackoffDelayFunc) - defer txBackoffTimer.Stop() + backoffTimer = newBackoffTimer(option.BackoffDelayFunc) + defer backoffTimer.Stop() } f := func() error { @@ -94,8 +94,8 @@ func RunInTxContext(ctx context.Context, db BeginTxer, opts *TxOptions, fn func( return err } - if txBackoffTimer != nil && i < option.MaxAttempts-1 { - if err = txBackoffTimer.Wait(ctx, i); err != nil { + if backoffTimer != nil && i < option.MaxAttempts-1 { + if err = backoffTimer.Wait(ctx, i); err != nil { return err } } @@ -109,7 +109,7 @@ type backoffTimer struct { backOffDelayFunc BackoffDelayFunc } -func newTxBackoffTimer(backoffDelayFunc BackoffDelayFunc) *backoffTimer { +func newBackoffTimer(backoffDelayFunc BackoffDelayFunc) *backoffTimer { return &backoffTimer{ timer: time.NewTimer(0), backOffDelayFunc: backoffDelayFunc, From 02fa35ee8487fa3956abfcf058ec72b6fce260bc Mon Sep 17 00:00:00 2001 From: Panot Wongkhot Date: Fri, 6 Jun 2025 00:32:29 +0700 Subject: [PATCH 6/9] move backoff to new package --- backoff.go => backoff/backoff.go | 16 ++++++----- backoff_test.go => backoff/backoff_test.go | 32 +++++++++++----------- 2 files changed, 25 insertions(+), 23 deletions(-) rename backoff.go => backoff/backoff.go (85%) rename backoff_test.go => backoff/backoff_test.go (82%) diff --git a/backoff.go b/backoff/backoff.go similarity index 85% rename from backoff.go rename to backoff/backoff.go index 16a8b28..6cc00f9 100644 --- a/backoff.go +++ b/backoff/backoff.go @@ -1,9 +1,11 @@ -package pgsql +package backoff import ( "math" "math/rand/v2" "time" + + "github.com/acoshift/pgsql" ) // BackoffConfig contains common configuration for all backoff strategies @@ -38,7 +40,7 @@ const ( ) // NewExponentialBackoff creates a new exponential backoff function -func NewExponentialBackoff(config ExponentialBackoffConfig) BackoffDelayFunc { +func NewExponentialBackoff(config ExponentialBackoffConfig) pgsql.BackoffDelayFunc { return func(attempt int) time.Duration { baseDelay := time.Duration(float64(config.BaseDelay) * math.Pow(config.Multiplier, float64(attempt))) if baseDelay > config.MaxDelay { @@ -71,7 +73,7 @@ func NewExponentialBackoff(config ExponentialBackoffConfig) BackoffDelayFunc { } // NewLinearBackoff creates a new linear backoff function -func NewLinearBackoff(config LinearBackoffConfig) BackoffDelayFunc { +func NewLinearBackoff(config LinearBackoffConfig) pgsql.BackoffDelayFunc { return func(attempt int) time.Duration { delay := config.BaseDelay + time.Duration(attempt)*config.Increment if delay > config.MaxDelay { @@ -81,7 +83,7 @@ func NewLinearBackoff(config LinearBackoffConfig) BackoffDelayFunc { } } -func DefaultExponentialBackoff() BackoffDelayFunc { +func DefaultExponentialBackoff() pgsql.BackoffDelayFunc { return NewExponentialBackoff(ExponentialBackoffConfig{ BackoffConfig: BackoffConfig{ BaseDelay: 100 * time.Millisecond, @@ -92,7 +94,7 @@ func DefaultExponentialBackoff() BackoffDelayFunc { }) } -func DefaultExponentialBackoffWithFullJitter() BackoffDelayFunc { +func DefaultExponentialBackoffWithFullJitter() pgsql.BackoffDelayFunc { return NewExponentialBackoff(ExponentialBackoffConfig{ BackoffConfig: BackoffConfig{ BaseDelay: 100 * time.Millisecond, @@ -103,7 +105,7 @@ func DefaultExponentialBackoffWithFullJitter() BackoffDelayFunc { }) } -func DefaultExponentialBackoffWithEqualJitter() BackoffDelayFunc { +func DefaultExponentialBackoffWithEqualJitter() pgsql.BackoffDelayFunc { return NewExponentialBackoff(ExponentialBackoffConfig{ BackoffConfig: BackoffConfig{ BaseDelay: 100 * time.Millisecond, @@ -114,7 +116,7 @@ func DefaultExponentialBackoffWithEqualJitter() BackoffDelayFunc { }) } -func DefaultLinearBackoff() BackoffDelayFunc { +func DefaultLinearBackoff() pgsql.BackoffDelayFunc { return NewLinearBackoff(LinearBackoffConfig{ BackoffConfig: BackoffConfig{ BaseDelay: 100 * time.Millisecond, diff --git a/backoff_test.go b/backoff/backoff_test.go similarity index 82% rename from backoff_test.go rename to backoff/backoff_test.go index 577d1e3..70b8bdb 100644 --- a/backoff_test.go +++ b/backoff/backoff_test.go @@ -1,23 +1,23 @@ -package pgsql_test +package backoff_test import ( "testing" "time" - "github.com/acoshift/pgsql" + "github.com/acoshift/pgsql/backoff" ) func TestExponentialBackoff(t *testing.T) { t.Parallel() - config := pgsql.ExponentialBackoffConfig{ - BackoffConfig: pgsql.BackoffConfig{ + config := backoff.ExponentialBackoffConfig{ + BackoffConfig: backoff.BackoffConfig{ BaseDelay: 10 * time.Millisecond, MaxDelay: 1 * time.Second, }, Multiplier: 2.0, } - backoff := pgsql.NewExponentialBackoff(config) + backoff := backoff.NewExponentialBackoff(config) // Test exponential growth delays := []time.Duration{} @@ -45,15 +45,15 @@ func TestExponentialBackoff(t *testing.T) { func TestExponentialBackoffWithFullJitter(t *testing.T) { t.Parallel() - config := pgsql.ExponentialBackoffConfig{ - BackoffConfig: pgsql.BackoffConfig{ + config := backoff.ExponentialBackoffConfig{ + BackoffConfig: backoff.BackoffConfig{ BaseDelay: 100 * time.Millisecond, MaxDelay: 1 * time.Second, }, Multiplier: 2.0, - JitterType: pgsql.FullJitter, + JitterType: backoff.FullJitter, } - backoff := pgsql.NewExponentialBackoff(config) + backoff := backoff.NewExponentialBackoff(config) // Test that jitter introduces randomness var delays []time.Duration @@ -86,15 +86,15 @@ func TestExponentialBackoffWithFullJitter(t *testing.T) { func TestExponentialBackoffWithEqualJitter(t *testing.T) { t.Parallel() - config := pgsql.ExponentialBackoffConfig{ - BackoffConfig: pgsql.BackoffConfig{ + config := backoff.ExponentialBackoffConfig{ + BackoffConfig: backoff.BackoffConfig{ BaseDelay: 100 * time.Millisecond, MaxDelay: 1 * time.Second, }, Multiplier: 2.0, - JitterType: pgsql.EqualJitter, + JitterType: backoff.EqualJitter, } - backoff := pgsql.NewExponentialBackoff(config) + backoff := backoff.NewExponentialBackoff(config) delay := backoff(2) @@ -116,14 +116,14 @@ func TestExponentialBackoffWithEqualJitter(t *testing.T) { func TestLinearBackoff(t *testing.T) { t.Parallel() - config := pgsql.LinearBackoffConfig{ - BackoffConfig: pgsql.BackoffConfig{ + config := backoff.LinearBackoffConfig{ + BackoffConfig: backoff.BackoffConfig{ BaseDelay: 100 * time.Millisecond, MaxDelay: 1 * time.Second, }, Increment: 100 * time.Millisecond, } - backoff := pgsql.NewLinearBackoff(config) + backoff := backoff.NewLinearBackoff(config) // Test linear growth delays := []time.Duration{} From ffcd402451340463fd4793b1cfcb64f26b154417 Mon Sep 17 00:00:00 2001 From: Panot Wongkhot Date: Fri, 6 Jun 2025 02:08:52 +0700 Subject: [PATCH 7/9] add tx retry test --- tx_test.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/tx_test.go b/tx_test.go index b2d15ed..ccb3e40 100644 --- a/tx_test.go +++ b/tx_test.go @@ -1,12 +1,16 @@ package pgsql_test import ( + "context" "database/sql" + "database/sql/driver" + "errors" "fmt" "log" "math/rand" "sync" "testing" + "time" "github.com/acoshift/pgsql" ) @@ -148,3 +152,155 @@ func TestTx(t *testing.T) { t.Fatalf("expected sum all value to be 0; got %d", result) } } + +func TestTxRetryWithBackoff(t *testing.T) { + t.Parallel() + + t.Run("Backoff when serialization failure occurs", func(t *testing.T) { + t.Parallel() + + attemptCount := 0 + opts := &pgsql.TxOptions{ + MaxAttempts: 3, + BackoffDelayFunc: func(attempt int) time.Duration { + attemptCount++ + return 1 + }, + } + + pgsql.RunInTxContext(context.Background(), sql.OpenDB(&fakeConnector{}), opts, func(*sql.Tx) error { + return &mockSerializationFailureError{} + }) + + if attemptCount != opts.MaxAttempts-1 { + t.Fatalf("expected BackoffDelayFunc to be called %d times, got %d", opts.MaxAttempts, attemptCount) + } + }) + + t.Run("Successful After Multiple Failures", func(t *testing.T) { + t.Parallel() + + failCount := 0 + maxFailures := 3 + opts := &pgsql.TxOptions{ + MaxAttempts: maxFailures + 1, + BackoffDelayFunc: func(attempt int) time.Duration { + return 1 + }, + } + + err := pgsql.RunInTxContext(context.Background(), sql.OpenDB(&fakeConnector{}), opts, func(tx *sql.Tx) error { + if failCount < maxFailures { + failCount++ + return &mockSerializationFailureError{} + } + return nil + }) + if err != nil { + t.Fatalf("expected success after failures, got error: %v", err) + } + if failCount != maxFailures { + t.Fatalf("expected %d failures before success, got %d", maxFailures, failCount) + } + }) + + t.Run("Context Cancellation", func(t *testing.T) { + t.Parallel() + + opts := &pgsql.TxOptions{ + MaxAttempts: 3, + BackoffDelayFunc: func(attempt int) time.Duration { + return 1 + }, + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel the context immediately + + err := pgsql.RunInTxContext(ctx, sql.OpenDB(&fakeConnector{}), opts, func(*sql.Tx) error { + return &mockSerializationFailureError{} + }) + if !errors.Is(err, context.Canceled) { + t.Fatalf("expected context.Canceled error, got %v", err) + } + }) + + t.Run("Max Attempts Reached", func(t *testing.T) { + t.Parallel() + + attemptCount := 0 + opts := &pgsql.TxOptions{ + MaxAttempts: 3, + BackoffDelayFunc: func(attempt int) time.Duration { + return 1 + }, + } + + err := pgsql.RunInTxContext(context.Background(), sql.OpenDB(&fakeConnector{}), opts, func(*sql.Tx) error { + attemptCount++ + return &mockSerializationFailureError{} + }) + if errors.As(err, &mockSerializationFailureError{}) { + t.Fatal("expected an error when max attempts reached") + } + if attemptCount != opts.MaxAttempts { + t.Fatalf("expected %d attempts, got %d", opts.MaxAttempts, attemptCount) + } + }) +} + +type fakeConnector struct { + driver.Connector +} + +func (c *fakeConnector) Connect(ctx context.Context) (driver.Conn, error) { + return &fakeConn{}, nil +} + +func (c *fakeConnector) Driver() driver.Driver { + panic("not implemented") +} + +type fakeConn struct { + driver.Conn +} + +func (c *fakeConn) Prepare(query string) (driver.Stmt, error) { + return nil, fmt.Errorf("not implemented") +} + +func (c *fakeConn) Close() error { + return nil +} + +func (c *fakeConn) Begin() (driver.Tx, error) { + return &fakeTx{}, nil +} + +var _ driver.ConnBeginTx = (*fakeConn)(nil) + +func (c *fakeConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { + return &fakeTx{}, nil +} + +type fakeTx struct { + driver.Tx +} + +func (tx *fakeTx) Commit() error { + return nil +} + +func (tx *fakeTx) Rollback() error { + return nil +} + +type mockSerializationFailureError struct{} + +func (e mockSerializationFailureError) Error() string { + return "mock serialization failure error" +} + +func (e mockSerializationFailureError) SQLState() string { + return "40001" // SQLSTATE code for serialization failure +} From 9a1148ebe5ade316e8df557e84328ddb6dc9ce00 Mon Sep 17 00:00:00 2001 From: Panot Wongkhot Date: Fri, 6 Jun 2025 02:20:53 +0700 Subject: [PATCH 8/9] reduce verbose --- backoff/backoff.go | 48 ++++++++++++++++++++--------------------- backoff/backoff_test.go | 30 +++++++++++++------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/backoff/backoff.go b/backoff/backoff.go index 6cc00f9..ecfed3e 100644 --- a/backoff/backoff.go +++ b/backoff/backoff.go @@ -8,22 +8,22 @@ import ( "github.com/acoshift/pgsql" ) -// BackoffConfig contains common configuration for all backoff strategies -type BackoffConfig struct { +// Config contains common configuration for all backoff strategies +type Config struct { BaseDelay time.Duration // Base delay for backoff MaxDelay time.Duration // Maximum delay cap } -// ExponentialBackoffConfig contains configuration for exponential backoff -type ExponentialBackoffConfig struct { - BackoffConfig +// ExponentialConfig contains configuration for exponential backoff +type ExponentialConfig struct { + Config Multiplier float64 // Multiplier for exponential growth JitterType JitterType } -// LinearBackoffConfig contains configuration for linear backoff -type LinearBackoffConfig struct { - BackoffConfig +// LinearConfig contains configuration for linear backoff +type LinearConfig struct { + Config Increment time.Duration // Amount to increase delay each attempt } @@ -39,8 +39,8 @@ const ( EqualJitter ) -// NewExponentialBackoff creates a new exponential backoff function -func NewExponentialBackoff(config ExponentialBackoffConfig) pgsql.BackoffDelayFunc { +// NewExponential creates a new exponential backoff function +func NewExponential(config ExponentialConfig) pgsql.BackoffDelayFunc { return func(attempt int) time.Duration { baseDelay := time.Duration(float64(config.BaseDelay) * math.Pow(config.Multiplier, float64(attempt))) if baseDelay > config.MaxDelay { @@ -72,8 +72,8 @@ func NewExponentialBackoff(config ExponentialBackoffConfig) pgsql.BackoffDelayFu } } -// NewLinearBackoff creates a new linear backoff function -func NewLinearBackoff(config LinearBackoffConfig) pgsql.BackoffDelayFunc { +// NewLinear creates a new linear backoff function +func NewLinear(config LinearConfig) pgsql.BackoffDelayFunc { return func(attempt int) time.Duration { delay := config.BaseDelay + time.Duration(attempt)*config.Increment if delay > config.MaxDelay { @@ -83,9 +83,9 @@ func NewLinearBackoff(config LinearBackoffConfig) pgsql.BackoffDelayFunc { } } -func DefaultExponentialBackoff() pgsql.BackoffDelayFunc { - return NewExponentialBackoff(ExponentialBackoffConfig{ - BackoffConfig: BackoffConfig{ +func DefaultExponential() pgsql.BackoffDelayFunc { + return NewExponential(ExponentialConfig{ + Config: Config{ BaseDelay: 100 * time.Millisecond, MaxDelay: 5 * time.Second, }, @@ -94,9 +94,9 @@ func DefaultExponentialBackoff() pgsql.BackoffDelayFunc { }) } -func DefaultExponentialBackoffWithFullJitter() pgsql.BackoffDelayFunc { - return NewExponentialBackoff(ExponentialBackoffConfig{ - BackoffConfig: BackoffConfig{ +func DefaultExponentialWithFullJitter() pgsql.BackoffDelayFunc { + return NewExponential(ExponentialConfig{ + Config: Config{ BaseDelay: 100 * time.Millisecond, MaxDelay: 5 * time.Second, }, @@ -105,9 +105,9 @@ func DefaultExponentialBackoffWithFullJitter() pgsql.BackoffDelayFunc { }) } -func DefaultExponentialBackoffWithEqualJitter() pgsql.BackoffDelayFunc { - return NewExponentialBackoff(ExponentialBackoffConfig{ - BackoffConfig: BackoffConfig{ +func DefaultExponentialWithEqualJitter() pgsql.BackoffDelayFunc { + return NewExponential(ExponentialConfig{ + Config: Config{ BaseDelay: 100 * time.Millisecond, MaxDelay: 5 * time.Second, }, @@ -116,9 +116,9 @@ func DefaultExponentialBackoffWithEqualJitter() pgsql.BackoffDelayFunc { }) } -func DefaultLinearBackoff() pgsql.BackoffDelayFunc { - return NewLinearBackoff(LinearBackoffConfig{ - BackoffConfig: BackoffConfig{ +func DefaultLinear() pgsql.BackoffDelayFunc { + return NewLinear(LinearConfig{ + Config: Config{ BaseDelay: 100 * time.Millisecond, MaxDelay: 5 * time.Second, }, diff --git a/backoff/backoff_test.go b/backoff/backoff_test.go index 70b8bdb..621f573 100644 --- a/backoff/backoff_test.go +++ b/backoff/backoff_test.go @@ -7,17 +7,17 @@ import ( "github.com/acoshift/pgsql/backoff" ) -func TestExponentialBackoff(t *testing.T) { +func TestExponential(t *testing.T) { t.Parallel() - config := backoff.ExponentialBackoffConfig{ - BackoffConfig: backoff.BackoffConfig{ + config := backoff.ExponentialConfig{ + Config: backoff.Config{ BaseDelay: 10 * time.Millisecond, MaxDelay: 1 * time.Second, }, Multiplier: 2.0, } - backoff := backoff.NewExponentialBackoff(config) + backoff := backoff.NewExponential(config) // Test exponential growth delays := []time.Duration{} @@ -42,18 +42,18 @@ func TestExponentialBackoff(t *testing.T) { } } -func TestExponentialBackoffWithFullJitter(t *testing.T) { +func TestExponentialWithFullJitter(t *testing.T) { t.Parallel() - config := backoff.ExponentialBackoffConfig{ - BackoffConfig: backoff.BackoffConfig{ + config := backoff.ExponentialConfig{ + Config: backoff.Config{ BaseDelay: 100 * time.Millisecond, MaxDelay: 1 * time.Second, }, Multiplier: 2.0, JitterType: backoff.FullJitter, } - backoff := backoff.NewExponentialBackoff(config) + backoff := backoff.NewExponential(config) // Test that jitter introduces randomness var delays []time.Duration @@ -83,18 +83,18 @@ func TestExponentialBackoffWithFullJitter(t *testing.T) { } } -func TestExponentialBackoffWithEqualJitter(t *testing.T) { +func TestExponentialWithEqualJitter(t *testing.T) { t.Parallel() - config := backoff.ExponentialBackoffConfig{ - BackoffConfig: backoff.BackoffConfig{ + config := backoff.ExponentialConfig{ + Config: backoff.Config{ BaseDelay: 100 * time.Millisecond, MaxDelay: 1 * time.Second, }, Multiplier: 2.0, JitterType: backoff.EqualJitter, } - backoff := backoff.NewExponentialBackoff(config) + backoff := backoff.NewExponential(config) delay := backoff(2) @@ -116,14 +116,14 @@ func TestExponentialBackoffWithEqualJitter(t *testing.T) { func TestLinearBackoff(t *testing.T) { t.Parallel() - config := backoff.LinearBackoffConfig{ - BackoffConfig: backoff.BackoffConfig{ + config := backoff.LinearConfig{ + Config: backoff.Config{ BaseDelay: 100 * time.Millisecond, MaxDelay: 1 * time.Second, }, Increment: 100 * time.Millisecond, } - backoff := backoff.NewLinearBackoff(config) + backoff := backoff.NewLinear(config) // Test linear growth delays := []time.Duration{} From 839942b03c9af163fcca9dc7f126fe4612e512c4 Mon Sep 17 00:00:00 2001 From: Panot Wongkhot Date: Fri, 4 Jul 2025 22:31:43 +0700 Subject: [PATCH 9/9] refactor backoffTimer to normal wait --- tx.go | 34 +++++----------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/tx.go b/tx.go index 200678d..905ff78 100644 --- a/tx.go +++ b/tx.go @@ -63,12 +63,6 @@ func RunInTxContext(ctx context.Context, db BeginTxer, opts *TxOptions, fn func( option.BackoffDelayFunc = opts.BackoffDelayFunc } - var backoffTimer *backoffTimer - if option.BackoffDelayFunc != nil { - backoffTimer = newBackoffTimer(option.BackoffDelayFunc) - defer backoffTimer.Stop() - } - f := func() error { tx, err := db.BeginTx(ctx, &option.TxOptions) if err != nil { @@ -94,8 +88,8 @@ func RunInTxContext(ctx context.Context, db BeginTxer, opts *TxOptions, fn func( return err } - if backoffTimer != nil && i < option.MaxAttempts-1 { - if err = backoffTimer.Wait(ctx, i); err != nil { + if i < option.MaxAttempts-1 && option.BackoffDelayFunc != nil { + if err = wait(ctx, i, option.BackoffDelayFunc); err != nil { return err } } @@ -104,34 +98,16 @@ func RunInTxContext(ctx context.Context, db BeginTxer, opts *TxOptions, fn func( return err } -type backoffTimer struct { - timer *time.Timer - backOffDelayFunc BackoffDelayFunc -} - -func newBackoffTimer(backoffDelayFunc BackoffDelayFunc) *backoffTimer { - return &backoffTimer{ - timer: time.NewTimer(0), - backOffDelayFunc: backoffDelayFunc, - } -} - -func (b *backoffTimer) Wait(ctx context.Context, attempt int) error { - delay := b.backOffDelayFunc(attempt) +func wait(ctx context.Context, attempt int, backOffDelayFunc BackoffDelayFunc) error { + delay := backOffDelayFunc(attempt) if delay <= 0 { return nil } - b.timer.Reset(delay) - select { case <-ctx.Done(): return ctx.Err() - case <-b.timer.C: + case <-time.After(delay): return nil } } - -func (b *backoffTimer) Stop() bool { - return b.timer.Stop() -}