| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- package grpcclient
- import (
- "context"
- "math"
- "time"
- "google.golang.org/grpc/codes"
- "google.golang.org/grpc/status"
- )
- const (
- backoffBase = 100 * time.Millisecond
- backoffMaxRetries = 8
- backoffMax = 30 * time.Second
- )
- // retryable returns true when the status code is a transient error worth
- // retrying within the retry budget.
- func retryable(code codes.Code) bool {
- switch code {
- case codes.Unavailable, codes.ResourceExhausted, codes.Internal:
- return true
- default:
- return false
- }
- }
- // doBackoff sleeps for an interval derived from attempt and the
- // server-supplied retryAfterMs. It respects context cancellation.
- func doBackoff(ctx context.Context, attempt int, retryAfterMs int) error {
- var delay time.Duration
- if retryAfterMs > 0 {
- delay = time.Duration(retryAfterMs) * time.Millisecond
- } else {
- delay = time.Duration(float64(backoffBase) * math.Pow(2, float64(attempt)))
- if delay > backoffMax {
- delay = backoffMax
- }
- }
- select {
- case <-ctx.Done():
- return ctx.Err()
- case <-time.After(delay):
- return nil
- }
- }
- // Retry calls fn repeatedly (up to c.maxRetries) until it succeeds,
- // ctx is cancelled, or a non-retryable error is returned.
- // fn should perform a single gRPC operation that may fail transiently.
- func (c *Client) Retry(ctx context.Context, fn func() error) error {
- var lastErr error
- for attempt := 0; attempt <= c.maxRetries; attempt++ {
- if err := ctx.Err(); err != nil {
- return err
- }
- lastErr = fn()
- if lastErr == nil {
- return nil
- }
- st, _ := status.FromError(lastErr)
- if !retryable(st.Code()) {
- return lastErr
- }
- if attempt < c.maxRetries {
- if err := doBackoff(ctx, attempt, 0); err != nil {
- return err
- }
- }
- }
- return lastErr
- }
|