Skip to content

Commit

Permalink
add retryafter func
Browse files Browse the repository at this point in the history
  • Loading branch information
cenkalti committed Dec 18, 2024
1 parent b41b52b commit 98570a5
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 34 deletions.
17 changes: 17 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package backoff

import (
"fmt"
"time"
)

// PermanentError signals that the operation should not be retried.
type PermanentError struct {
Err error
Expand All @@ -22,3 +27,15 @@ func (e *PermanentError) Error() string {
func (e *PermanentError) Unwrap() error {
return e.Err
}

type RetryAfterError struct {
Duration time.Duration
}

func RetryAfter(seconds int) error {
return &RetryAfterError{Duration: time.Duration(seconds) * time.Second}
}

func (e *RetryAfterError) Error() string {
return fmt.Sprintf("retry after %s", e.Duration)
}
65 changes: 31 additions & 34 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,51 @@ package backoff

import (
"context"
"errors"
"fmt"
"log"
"net/http"
"strconv"
)

func ExampleRetry() {
// An operation that may fail.
// Define an operation function that returns a value and an error.
// The value can be any type.
// We'll pass this operation to Retry function.
operation := func() (string, error) {
return "hello", nil
}

val, err := Retry(context.TODO(), operation, WithBackOff(NewExponentialBackOff()))
if err != nil {
// Handle error.
return
}

// Operation is successful.

fmt.Println(val)
// Output: hello
}

func ExampleTicker() {
// An operation that may fail.
operation := func() error {
return nil // or an error
}

ticker := NewTicker(NewExponentialBackOff())
// An example request that may fail.
resp, err := http.Get("http://httpbin.org/get")
if err != nil {
return "", err
}
defer resp.Body.Close()

var err error
// In case on non-retriable error, return Permanent error to stop retrying.
// For this HTTP example, client errors are non-retriable.
if resp.StatusCode == 400 {
return "", Permanent(errors.New("bad request"))
}

// Ticks will continue to arrive when the previous operation is still running,
// so operations that take a while to fail could run in quick succession.
for range ticker.C {
if err = operation(); err != nil {
log.Println(err, "will retry...")
continue
// If we are being rate limited, return a RetryAfter to specify how long to wait.
// This will also reset the backoff policy.
if resp.StatusCode == 429 {
seconds, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
if err == nil {
return "", RetryAfter(int(seconds))
}
}

ticker.Stop()
break
// Return successful response.
return "hello", nil
}

result, err := Retry(context.TODO(), operation, WithBackOff(NewExponentialBackOff()))
if err != nil {
// Operation has failed.
fmt.Println("Error:", err)
return
}

// Operation is successful.

fmt.Println(result)
// Output: hello
}
7 changes: 7 additions & 0 deletions retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ func Retry[T any](ctx context.Context, operation Operation[T], opts ...RetryOpti
return res, err
}

// Reset backoff if RetryAfterError is encountered.
var retryAfter *RetryAfterError
if errors.As(err, &retryAfter) {
next = retryAfter.Duration
args.BackOff.Reset()
}

// Stop retrying if context is cancelled.
if cerr := ctx.Err(); cerr != nil {
return res, cerr
Expand Down

0 comments on commit 98570a5

Please sign in to comment.