retry/retry.go (62 lines of code) (raw):
// Copyright 2024 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// https://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package retry implements retry logic helpers to execute arbitrary functions with defined policy.
package retry
import (
"context"
"fmt"
"math"
"time"
"github.com/GoogleCloudPlatform/guest-logging-go/logger"
)
// IsRetriable is method signature for implementing to override default logic of retrying each error.
type IsRetriable func(error) bool
// Policy represents the struct to configure the retry behavior.
type Policy struct {
// MaxAttempts represents the maximum number of retry attempts.
MaxAttempts int
// BackoffFactor is the multiplier by which retry interval (Jitter) increases after each retry.
// For constant backoff set Backoff factor to 1.
BackoffFactor float64
// Jitter is the interval before the first retry.
Jitter time.Duration
// ShouldRetry is optional and the way to override default retry logic of retry every error.
// If ShouldRetry is not provided/implemented every error will be retried until all attempts are exhausted.
ShouldRetry IsRetriable
}
// backoff computes interval between retries. Interval is jitter*(backoffFactor^attempt).
// For e.g. if jitter was set to 10 and factor was 3, backoff between attempts would be [10, 30, 90, 270...].
func backoff(attempt int, policy Policy) time.Duration {
b := float64(policy.Jitter) * math.Pow(policy.BackoffFactor, float64(attempt))
return time.Duration(b)
}
// isRetriable checks if error is retriable. If ShouldRetry is unimplemented it always returns
// true, otherwise overriden method's logic determines the retry behavior.
func isRetriable(policy Policy, err error) bool {
if policy.ShouldRetry == nil {
return true
}
return policy.ShouldRetry(err)
}
// RunWithResponse executes and retries the function on failure based on policy defined and returns response on success.
func RunWithResponse[T any](ctx context.Context, policy Policy, f func() (T, error)) (T, error) {
var (
res T
err error
)
if f == nil {
return res, fmt.Errorf("retry function cannot be nil")
}
for attempt := 0; attempt < policy.MaxAttempts; attempt++ {
if res, err = f(); err == nil {
return res, nil
}
if err != nil && !isRetriable(policy, err) {
return res, fmt.Errorf("giving up, retry policy returned false on error: %+v", err)
}
logger.Debugf("Attempt %d failed with error %+v", attempt, err)
// Return early, no need to wait if all retries have exhausted.
if attempt+1 >= policy.MaxAttempts {
return res, fmt.Errorf("exhausted all (%d) retries, last error: %+v", policy.MaxAttempts, err)
}
select {
case <-ctx.Done():
return res, ctx.Err()
case <-time.After(backoff(attempt, policy)):
}
}
return res, fmt.Errorf("num of retries set to 0, made no attempts to run")
}
// Run executes and retries the function on failure based on policy defined and returns nil-error on success.
func Run(ctx context.Context, policy Policy, f func() error) error {
if f == nil {
return fmt.Errorf("retry function cannot be nil")
}
fn := func() (any, error) {
return nil, f()
}
_, err := RunWithResponse(ctx, policy, fn)
return err
}