103 lines
2.8 KiB
Go
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)
|
|
}
|