mock: Go Coverage Report

not tracked not covered covered

    //    \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
    //  \\\\\ Copyright 2024-present SPIKE contributors.
    // \\\\\\\ SPDX-License-Identifier: Apache-2.0

    package mock

    import (
            "context"
    )

    // Retrier implements Retrier for testing
    type Retrier struct {
            RetryFunc func(context.Context, func() error) error
    }

    // RetryWithBackoff implements the Retrier interface
    func (m *Retrier) RetryWithBackoff(
            ctx context.Context,
            operation func() error,
    ) error {
            if m.RetryFunc != nil {
                    return m.RetryFunc(ctx, operation)
            }
            return nil
    }

    //    \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/
    //  \\\\\ Copyright 2024-present SPIKE contributors.
    // \\\\\\\ SPDX-License-Identifier: Apache-2.0

    // Package retry provides a flexible and type-safe retry mechanism with
    // exponential backoff. It allows for customizable retry strategies and
    // notifications while maintaining context awareness and cancellation support.
    package retry

    import (
            "context"
            "errors"
            "time"

            "github.com/cenkalti/backoff/v4"
            sdkErrors "github.com/spiffe/spike-sdk-go/errors"
    )

    // Default configuration values for the exponential backoff strategy
    const (
            // Initial wait time between retries
            defaultInitialInterval = 500 * time.Millisecond
            // Maximum wait time between retries
            defaultMaxInterval = 60 * time.Second
            // Maximum total time for all retry attempts
            defaultMaxElapsedTime = 1200 * time.Second
            // A zero max elapsed time means try forever.
            forever = 0
            // Factor by which the wait time increases
            defaultMultiplier = 2.0
    )

    // Retrier defines the interface for retry operations with backoff support.
    // Implementations of this interface provide different retry strategies.
    type Retrier interface {
            // RetryWithBackoff executes an operation with a backoff strategy.
            // It will repeatedly execute the operation until it succeeds or
            // the context is canceled. The backoff strategy determines the
            // delay between retry attempts.
            //
            // Parameters:
            //   - ctx: Context for cancellation and timeout control
            //   - op: The operation to retry, returns error if the attempt failed
            //
            // Returns:
            //   - *sdkErrors.SDKError: nil if successful, or one of the following:
            //   - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached
            //   - ErrRetryContextCanceled: if context is canceled
            //   - The last error from the operation
            RetryWithBackoff(
                    ctx context.Context, op func() *sdkErrors.SDKError,
            ) *sdkErrors.SDKError
    }

    // TypedRetrier provides type-safe retry operations for functions that return
    // both a value and an error. It wraps a base Retrier to provide typed results.
    type TypedRetrier[T any] struct {
            retrier Retrier
    }

    // NewTypedRetrier creates a new TypedRetrier with the given base Retrier.
    // This allows for type-safe retry operations while reusing existing retry
    // logic.
    //
    // Parameters:
    //   - r: The base Retrier implementation to wrap
    //
    // Returns:
    //   - *TypedRetrier[T]: A new TypedRetrier instance for the specified type
    //
    // Example:
    //
    //                retrier := NewTypedRetrier[string](NewExponentialRetrier())
    //                result, err := retrier.RetryWithBackoff(ctx, func() (
    //                 string, *sdkErrors.SDKError) {
    //                    return callExternalService()
    //                })
    func NewTypedRetrier[T any](r Retrier) *TypedRetrier[T] {
            return &TypedRetrier[T]{retrier: r}
    }

    // RetryWithBackoff executes a typed operation with a backoff strategy.
    // It preserves the return value while maintaining retry functionality.
    //
    // Parameters:
    //   - ctx: Context for cancellation and timeout control
    //   - op: The operation to retry, returns both a value and an error
    //
    // Returns:
    //   - T: The result value from the successful operation
    //   - *sdkErrors.SDKError: nil if successful, or one of the following errors:
    //   - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached
    //   - ErrRetryContextCanceled: if context is canceled
    //   - The wrapped error from the operation if it fails
    func (r *TypedRetrier[T]) RetryWithBackoff(
            ctx context.Context,
            op func() (T, *sdkErrors.SDKError),
    ) (T, *sdkErrors.SDKError) {
            var result T
            err := r.retrier.RetryWithBackoff(ctx, func() *sdkErrors.SDKError {
                    var opErr *sdkErrors.SDKError
                    result, opErr = op()
                    return opErr
            })
            return result, err
    }

    // NotifyFn is a callback function type for retry notifications.
    // It provides information about each retry attempt, including the error,
    // current interval duration, and total elapsed time.
    type NotifyFn func(
            err *sdkErrors.SDKError, duration, totalDuration time.Duration,
    )

    // RetrierOption is a function type for configuring ExponentialRetrier.
    // It follows the functional options pattern for flexible configuration.
    type RetrierOption func(*ExponentialRetrier)

    // ExponentialRetrier implements Retrier using exponential backoff strategy.
    // It provides configurable retry intervals and maximum attempt durations.
    type ExponentialRetrier struct {
            newBackOff func() backoff.BackOff
            notify     NotifyFn
    }

    // BackOffOption is a function type for configuring ExponentialBackOff.
    // It allows fine-tuning of the backoff strategy parameters.
    type BackOffOption func(*backoff.ExponentialBackOff)

    // NewExponentialRetrier creates a new ExponentialRetrier with configurable
    // settings. Default values provide sensible backoff behavior for most use
    // cases.
    //
    // Default settings:
    //   - InitialInterval: 500ms
    //   - MaxInterval: 60s
    //   - MaxElapsedTime: 1200s (20 minutes)
    //   - Multiplier: 2.0
    //
    // Parameters:
    //   - opts: Optional configuration functions to customize retry behavior
    //
    // Returns:
    //   - *ExponentialRetrier: A configured retrier instance ready for use
    //
    // Example:
    //
    //        retrier := NewExponentialRetrier(
    //            WithBackOffOptions(
    //                WithInitialInterval(100 * time.Millisecond),
    //                WithMaxInterval(5 * time.Second),
    //            ),
    //            WithNotify(func(err *sdkErrors.SDKError, d, total time.Duration) {
    //                log.Printf("Retry attempt failed: %v", err)
    //            }),
    //        )
    func NewExponentialRetrier(opts ...RetrierOption) *ExponentialRetrier {
            b := backoff.NewExponentialBackOff()
            b.InitialInterval = defaultInitialInterval
            b.MaxInterval = defaultMaxInterval
            b.MaxElapsedTime = defaultMaxElapsedTime
            b.Multiplier = defaultMultiplier

            r := &ExponentialRetrier{
                    newBackOff: func() backoff.BackOff {
                            return b
                    },
            }

            for _, opt := range opts {
                    opt(r)
            }

            return r
    }

    // RetryWithBackoff implements the Retrier interface using exponential backoff.
    // It executes the operation repeatedly until success or context cancellation.
    //
    // Parameters:
    //   - ctx: Context for cancellation and timeout control
    //   - operation: The function to retry that returns an error
    //
    // Returns:
    //   - *sdkErrors.SDKError: nil if the operation eventually succeeds, or one of:
    //   - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached
    //   - ErrRetryContextCanceled: if context is canceled
    //   - The last error from the operation
    func (r *ExponentialRetrier) RetryWithBackoff(
            ctx context.Context,
            operation func() *sdkErrors.SDKError,
    ) *sdkErrors.SDKError {
            b := r.newBackOff()
            totalDuration := time.Duration(0)

            // Wrap operation to convert SDKError to plain error for backoff library
            wrappedOp := func() error {
                    sdkErr := operation()
                    if sdkErr == nil {
                            return nil
                    }
                    return sdkErr
            }

            err := backoff.RetryNotify(
                    wrappedOp,
                    backoff.WithContext(b, ctx),
                    func(err error, duration time.Duration) {
                            totalDuration += duration
                            if r.notify != nil {
                                    // Convert plain error back to SDKError for notification
                                    var sdkErr *sdkErrors.SDKError
                                    if errors.As(err, &sdkErr) {
                                            r.notify(sdkErr, duration, totalDuration)
                                    } else {
                                            // Wrap plain error if it's not already an SDKError
                                            wrapped := sdkErrors.ErrRetryOperationFailed.Wrap(err)
                                            r.notify(wrapped, duration, totalDuration)
                                    }
                            }
                    },
            )

            if err == nil {
                    return nil
            }

            // Check if error is already an SDKError
            var sdkErr *sdkErrors.SDKError
            if errors.As(err, &sdkErr) {
                    return sdkErr
            }

            // Wrap context errors appropriately
            if errors.Is(err, context.Canceled) {
                    failErr := sdkErrors.ErrRetryContextCanceled.Wrap(err)
                    failErr.Msg = "retry operation canceled"
                    return failErr
            }

            if errors.Is(err, context.DeadlineExceeded) {
                    failErr := sdkErrors.ErrRetryMaxElapsedTimeReached.Wrap(err)
                    failErr.Msg = "maximum retry elapsed time exceeded"
                    return failErr
            }

            // Wrap any other error
            failErr := sdkErrors.ErrRetryOperationFailed.Wrap(err)
            failErr.Msg = "retry operation failed"
            return failErr
    }

    // WithBackOffOptions configures the backoff settings using the provided
    // options. Multiple options can be combined to customize the retry behavior.
    //
    // Parameters:
    //   - opts: One or more BackOffOption functions to configure the backoff
    //     strategy
    //
    // Returns:
    //   - RetrierOption: A configuration function for ExponentialRetrier
    //
    // Example:
    //
    //        retrier := NewExponentialRetrier(
    //            WithBackOffOptions(
    //                WithInitialInterval(1 * time.Second),
    //                WithMaxElapsedTime(1 * time.Minute),
    //            ),
    //        )
    func WithBackOffOptions(opts ...BackOffOption) RetrierOption {
            return func(r *ExponentialRetrier) {
                    b := r.newBackOff().(*backoff.ExponentialBackOff)
                    for _, opt := range opts {
                            opt(b)
                    }
            }
    }

    // WithInitialInterval sets the initial interval between retries.
    // This is the starting point for the exponential backoff calculation.
    //
    // Parameters:
    //   - d: The initial wait duration before the first retry
    //
    // Returns:
    //   - BackOffOption: A configuration function for ExponentialBackOff
    func WithInitialInterval(d time.Duration) BackOffOption {
            return func(b *backoff.ExponentialBackOff) {
                    b.InitialInterval = d
            }
    }

    // WithMaxInterval sets the maximum interval between retries.
    // The interval will never exceed this value, regardless of the multiplier.
    //
    // Parameters:
    //   - d: The maximum wait duration between retry attempts
    //
    // Returns:
    //   - BackOffOption: A configuration function for ExponentialBackOff
    func WithMaxInterval(d time.Duration) BackOffOption {
            return func(b *backoff.ExponentialBackOff) {
                    b.MaxInterval = d
            }
    }

    // WithMaxElapsedTime sets the maximum total time for retries.
    // The retry operation will stop after this duration, even if not successful.
    // Set to 0 to retry indefinitely (until context is canceled).
    //
    // Parameters:
    //   - d: The maximum total duration for all retry attempts
    //
    // Returns:
    //   - BackOffOption: A configuration function for ExponentialBackOff
    func WithMaxElapsedTime(d time.Duration) BackOffOption {
            return func(b *backoff.ExponentialBackOff) {
                    b.MaxElapsedTime = d
            }
    }

    // WithMultiplier sets the multiplier for increasing intervals.
    // Each retry interval is multiplied by this value, up to MaxInterval.
    //
    // Parameters:
    //   - m: The multiplier factor (e.g., 2.0 doubles the interval each time)
    //
    // Returns:
    //   - BackOffOption: A configuration function for ExponentialBackOff
    func WithMultiplier(m float64) BackOffOption {
            return func(b *backoff.ExponentialBackOff) {
                    b.Multiplier = m
            }
    }

    // WithRandomizationFactor sets the randomization factor for backoff intervals.
    // The actual interval will be randomized between
    // [interval * (1 - factor), interval * (1 + factor)].
    //
    // A factor of 0 disables randomization (deterministic intervals).
    // A factor of 0.5 (the default) means intervals can vary by ±50%.
    // This randomization helps prevent thundering herd issues in distributed
    // systems.
    //
    // Parameters:
    //   - factor: The randomization factor (0.0 to 1.0)
    //
    // Returns:
    //   - BackOffOption: A configuration function for ExponentialBackOff
    func WithRandomizationFactor(factor float64) BackOffOption {
            return func(b *backoff.ExponentialBackOff) {
                    b.RandomizationFactor = factor
            }
    }

    // WithNotify is an option to set the notification callback.
    // The callback is called after each failed attempt, allowing you to log
    // or monitor retry behavior.
    //
    // Parameters:
    //   - fn: Callback function invoked after each failed retry attempt
    //
    // Returns:
    //   - RetrierOption: A configuration function for ExponentialRetrier
    //
    // Example:
    //
    //        retrier := NewExponentialRetrier(
    //            WithNotify(func(err *sdkErrors.SDKError, d, total time.Duration) {
    //                log.Printf("Attempt failed after %v, total time %v: %v",
    //                    d, total, err)
    //            }),
    //        )
    func WithNotify(fn NotifyFn) RetrierOption {
            return func(r *ExponentialRetrier) {
                    r.notify = fn
            }
    }

    // Handler represents a function that returns a value and an error.
    // It's used with the Do helper function for simple retry operations.
    type Handler[T any] func() (T, *sdkErrors.SDKError)

    // Do provides a simplified way to retry a typed operation with configurable
    // settings. It creates a TypedRetrier with exponential backoff and applies
    // any provided options.
    //
    // This is a convenience function for common retry scenarios where you don't
    // need to create and manage a retrier instance explicitly.
    //
    // Parameters:
    //   - ctx: Context for cancellation and timeout control
    //   - handler: The function to retry that returns a value and error
    //   - options: Optional configuration for the retry behavior
    //
    // Returns:
    //   - T: The result value from the successful operation
    //   - *sdkErrors.SDKError: nil if successful, or one of the following errors:
    //   - ErrRetryMaxElapsedTimeReached: if maximum elapsed time is reached
    //   - ErrRetryContextCanceled: if context is canceled
    //   - The wrapped error from the handler if it fails
    //
    // Example:
    //
    //        result, err := Do(ctx, func() (string, *sdkErrors.SDKError) {
    //            return fetchData()
    //        }, WithNotify(logRetryAttempts))
    func Do[T any](
            ctx context.Context, handler Handler[T], options ...RetrierOption,
    ) (T, *sdkErrors.SDKError) {
            return NewTypedRetrier[T](
                    NewExponentialRetrier(options...),
            ).RetryWithBackoff(ctx, handler)
    }

    // Forever retries an operation indefinitely with exponential backoff until it
    // succeeds or the context is canceled. It sets MaxElapsedTime to 0, which means
    // the retry loop will continue forever (or until the context is canceled).
    //
    // This is a convenience function that sets up exponential backoff with sensible
    // defaults for infinite retry scenarios.
    //
    // Default settings:
    //   - InitialInterval: 500ms
    //   - MaxInterval: 60s
    //   - MaxElapsedTime: 0 (retry forever)
    //   - Multiplier: 2.0
    //
    // Parameters:
    //   - ctx: Context for cancellation control (the only way to stop retrying)
    //   - handler: The function to retry that returns a value and error
    //   - options: Optional configuration for retry behavior
    //
    // Note: User-provided options are applied AFTER the default settings and will
    // override them. If you pass WithBackOffOptions(WithMaxElapsedTime(...)), it
    // will override the "forever" behavior. This allows power users to customize
    // the retry behavior while keeping the convenience of preset defaults.
    //
    // Returns:
    //   - T: The result value from the successful operation
    //   - *sdkErrors.SDKError: nil if successful, or one of the following errors:
    //   - ErrRetryContextCanceled: if context is canceled
    //   - The wrapped error from the handler if all retries fail
    //
    // Example:
    //
    //                // Retry forever with custom notification
    //                result, err := Forever(ctx, func() (string, *sdkErrors.SDKError) {
    //                    return fetchData()
    //                }, WithNotify(func(err *sdkErrors.SDKError, d, total time.Duration) {
    //                    log.Printf("Retry failed: %v (attempt duration: %v, total: %v)",
    //                      err, d, total)
    //                }))
    //
    //                // Override behavior (will now stop after 1 minute
    //         //        instead of retrying forever)
    //                result, err := Forever(ctx, func() (string, *sdkErrors.SDKError) {
    //                    return fetchData()
    //                }, WithBackOffOptions(WithMaxElapsedTime(1 * time.Minute)))
    func Forever[T any](
            ctx context.Context, handler Handler[T], options ...RetrierOption,
    ) (T, *sdkErrors.SDKError) {
            ro := WithBackOffOptions(WithMaxElapsedTime(forever))
            ros := []RetrierOption{ro}
            ros = append(ros, options...)

            return NewTypedRetrier[T](
                    NewExponentialRetrier(ros...),
            ).RetryWithBackoff(ctx, handler)
    }