kamilsk/retry

View on GitHub
strategy/strategy.go

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
// Package strategy provides a way to define how retry is performed.
package strategy

import "time"

// A Breaker carries a cancellation signal to interrupt an action execution.
//
// It is a subset of the built-in context and github.com/kamilsk/breaker interfaces.
type Breaker = interface {
    // Done returns a channel that's closed when a cancellation signal occurred.
    Done() <-chan struct{}
    // If Done is not yet closed, Err returns nil.
    // If Done is closed, Err returns a non-nil error.
    // After Err returns a non-nil error, successive calls to Err return the same error.
    Err() error
}

// Strategy defines a function that Retry calls before every successive attempt
// to determine whether it should make the next attempt or not. Returning true
// allows for the next attempt to be made. Returning false halts the retrying
// process and returns the last error returned by the called Action.
//
// The strategy will be passed an "attempt" number on each successive retry
// iteration, starting with a 0 value before the first attempt is actually
// made. This allows for a pre-action delay, etc.
type Strategy = func(breaker Breaker, attempt uint, err error) bool

// Limit creates a Strategy that limits the number of attempts
// that Retry will make.
func Limit(value uint) Strategy {
    return func(_ Breaker, attempt uint, _ error) bool {
        return attempt < value
    }
}

// Delay creates a Strategy that waits the given duration
// before the first attempt is made.
func Delay(duration time.Duration) Strategy {
    return func(breaker Breaker, attempt uint, _ error) bool {
        keep := true
        if attempt == 0 {
            timer := time.NewTimer(duration)
            select {
            case <-timer.C:
            case <-breaker.Done():
                keep = false
            }
            stop(timer)
        }
        return keep
    }
}

// Wait creates a Strategy that waits the given durations for each attempt after
// the first. If the number of attempts is greater than the number of durations
// provided, then the strategy uses the last duration provided.
func Wait(durations ...time.Duration) Strategy {
    return func(breaker Breaker, attempt uint, _ error) bool {
        keep := true
        if attempt > 0 && len(durations) > 0 {
            durationIndex := int(attempt - 1)
            if len(durations) <= durationIndex {
                durationIndex = len(durations) - 1
            }
            timer := time.NewTimer(durations[durationIndex])
            select {
            case <-timer.C:
            case <-breaker.Done():
                keep = false
            }
            stop(timer)
        }
        return keep
    }
}

// Backoff creates a Strategy that waits before each attempt, with a duration as
// defined by the given backoff.Algorithm.
func Backoff(algorithm func(attempt uint) time.Duration) Strategy {
    return BackoffWithJitter(algorithm, func(duration time.Duration) time.Duration {
        return duration
    })
}

// BackoffWithJitter creates a Strategy that waits before each attempt, with a
// duration as defined by the given backoff.Algorithm and jitter.Transformation.
func BackoffWithJitter(
    algorithm func(attempt uint) time.Duration,
    transformation func(duration time.Duration) time.Duration,
) Strategy {
    return func(breaker Breaker, attempt uint, _ error) bool {
        keep := true
        if attempt > 0 {
            timer := time.NewTimer(transformation(algorithm(attempt)))
            select {
            case <-timer.C:
            case <-breaker.Done():
                keep = false
            }
            stop(timer)
        }
        return keep
    }
}

func stop(timer *time.Timer) {
    if !timer.Stop() {
        select {
        case <-timer.C:
        default:
        }
    }
}