Ctrl K
ComunidadGuías…

pkg/utils/retry.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package utils

import (
	"context"
	"math/rand/v2"
	"time"
)

const maxRetryDelay = time.Duration(1<<63 - 1)

// RetryOptions configures RetryWithBackoff.
type RetryOptions struct {
	// Attempts is the maximum number of calls to fn. Values lower than 1 are
	// treated as 1.
	Attempts int
	// Delay is the wait duration before the first retry. It doubles after each
	// failed attempt.
	Delay time.Duration
	// MaxDelay caps the exponential backoff delay before jitter is applied.
	MaxDelay time.Duration
	// JitterFactor adds a random delay between 0 and the current backoff delay
	// multiplied by JitterFactor. This jitter is one-sided: it can only extend
	// the delay, never shorten it. For example, 0.25 adds up to 25% jitter.
	// Values lower than or equal to 0 disable jitter.
	JitterFactor float64
	// ShouldRetry can be used to retry only some errors. When nil, every
	// non-nil error is retried until Attempts is exhausted.
	ShouldRetry func(error) bool
	// OnRetry is called after a failed attempt and before sleeping. The attempt
	// argument is the number of attempts already made, starting at 1.
	OnRetry func(attempt int, err error, delay time.Duration)
}

// RetryWithBackoff calls fn until it succeeds, ShouldRetry rejects the returned
// error, the context is done, or the maximum number of attempts is reached.
func RetryWithBackoff(ctx context.Context, opts RetryOptions, fn func() error) error {
	_, err := RetryWithBackoffValue(ctx, opts, func() (struct{}, error) {
		return struct{}{}, fn()
	})
	return err
}

// RetryWithBackoffValue calls fn until it succeeds, ShouldRetry rejects the
// returned error, the context is done, or the maximum number of attempts is
// reached. It returns the value returned by the successful call.
func RetryWithBackoffValue[T any](ctx context.Context, opts RetryOptions, fn func() (T, error)) (T, error) {
	var zero T
	if ctx == nil {
		ctx = context.Background()
	}

	attempts := opts.Attempts
	if attempts < 1 {
		attempts = 1
	}

	var err error
	var value T
	for attempt := 0; attempt < attempts; attempt++ {
		select {
		case <-ctx.Done():
			return zero, ctx.Err()
		default:
		}

		value, err = fn()
		if err == nil {
			return value, nil
		}
		if attempt == attempts-1 {
			return zero, err
		}
		if opts.ShouldRetry != nil && !opts.ShouldRetry(err) {
			return zero, err
		}

		delay := retryDelay(opts.Delay, attempt, opts.MaxDelay, opts.JitterFactor)
		if opts.OnRetry != nil {
			opts.OnRetry(attempt+1, err, delay)
		}
		if err := sleepWithContext(ctx, delay); err != nil {
			return zero, err
		}
	}

	return zero, err
}

// RetryWithExpBackoff can be used to call several times a function until it
// returns no error or the maximum count of calls has been reached. Between two
// calls, it will wait, first by the given delay, and after that, the delay
// will double after each failure.
func RetryWithExpBackoff(count int, delay time.Duration, fn func() error) error {
	return RetryWithBackoff(context.Background(), RetryOptions{
		Attempts: count,
		Delay:    delay,
	}, fn)
}

func retryDelay(initial time.Duration, attempt int, maxDelay time.Duration, jitterFactor float64) time.Duration {
	delay := backoffDelay(initial, attempt, maxDelay)
	return addJitter(delay, jitterFactor)
}

func backoffDelay(initial time.Duration, attempt int, maxDelay time.Duration) time.Duration {
	if initial <= 0 {
		return 0
	}

	delay := initial
	for i := 0; i < attempt; i++ {
		if delay > maxRetryDelay/2 {
			return cappedDelay(maxRetryDelay, maxDelay)
		}
		delay *= 2
	}
	return cappedDelay(delay, maxDelay)
}

func cappedDelay(delay time.Duration, maxDelay time.Duration) time.Duration {
	if maxDelay > 0 && delay > maxDelay {
		return maxDelay
	}
	return delay
}

func addJitter(delay time.Duration, jitterFactor float64) time.Duration {
	if delay <= 0 || jitterFactor <= 0 {
		return delay
	}

	maxJitterFloat := float64(delay) * jitterFactor
	if maxJitterFloat < 1 {
		return delay
	}
	var maxJitter time.Duration
	if maxJitterFloat >= float64(maxRetryDelay) {
		maxJitter = maxRetryDelay
	} else {
		maxJitter = time.Duration(maxJitterFloat)
	}
	if maxJitter <= 0 {
		return delay
	}
	jitter := time.Duration(rand.Int64N(int64(maxJitter)))
	if maxRetryDelay-delay < jitter {
		return maxRetryDelay
	}
	return delay + jitter
}

func sleepWithContext(ctx context.Context, delay time.Duration) error {
	if delay <= 0 {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
			return nil
		}
	}

	timer := time.NewTimer(delay)
	defer timer.Stop()

	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}