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 }