2019-09-23 21:30:20 +03:00

103 lines
2.8 KiB
Go

// Package retry provides the most advanced interruptible mechanism
// to perform actions repetitively until successful.
package retry
import (
"context"
"sync/atomic"
)
// Retry takes action and performs it, repetitively, until successful.
// When it is done it releases resources associated with the Breaker.
//
// Optionally, strategies may be passed that assess whether or not an attempt
// should be made.
func Retry(
breaker BreakCloser,
action func(attempt uint) error,
strategies ...func(attempt uint, err error) bool,
) error {
err := retry(breaker, action, strategies...)
breaker.Close()
return err
}
// Try takes action and performs it, repetitively, until successful.
//
// Optionally, strategies may be passed that assess whether or not an attempt
// should be made.
func Try(
breaker Breaker,
action func(attempt uint) error,
strategies ...func(attempt uint, err error) bool,
) error {
return retry(breaker, action, strategies...)
}
// TryContext takes action and performs it, repetitively, until successful.
// It uses the Context as a Breaker to prevent unnecessary action execution.
//
// Optionally, strategies may be passed that assess whether or not an attempt
// should be made.
func TryContext(
ctx context.Context,
action func(ctx context.Context, attempt uint) error,
strategies ...func(attempt uint, err error) bool,
) error {
cascade, cancel := context.WithCancel(ctx)
err := retry(ctx, currying(cascade, action), strategies...)
cancel()
return err
}
func currying(ctx context.Context, action func(context.Context, uint) error) func(uint) error {
return func(attempt uint) error { return action(ctx, attempt) }
}
func retry(
breaker Breaker,
action func(attempt uint) error,
strategies ...func(attempt uint, err error) bool,
) error {
var interrupted uint32
done := make(chan result, 1)
go func(breaker *uint32) {
var err error
defer func() {
done <- result{err, recover()}
close(done)
}()
for attempt := uint(0); shouldAttempt(breaker, attempt, err, strategies...); attempt++ {
err = action(attempt)
}
}(&interrupted)
select {
case <-breaker.Done():
atomic.CompareAndSwapUint32(&interrupted, 0, 1)
return Interrupted
case err := <-done:
if _, is := IsRecovered(err); is {
return err
// TODO:v5 throw origin
// panic(origin)
}
return err.error
}
}
// shouldAttempt evaluates the provided strategies with the given attempt to
// determine if the Retry loop should make another attempt.
func shouldAttempt(breaker *uint32, attempt uint, err error, strategies ...func(uint, error) bool) bool {
should := attempt == 0 || err != nil
for i, repeat := 0, len(strategies); should && i < repeat; i++ {
should = should && strategies[i](attempt, err)
}
return should && !atomic.CompareAndSwapUint32(breaker, 1, 0)
}