| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package cloud |
| |
| import ( |
| "context" |
| "log" |
| "sync/atomic" |
| "time" |
| ) |
| |
| // Fatal implements a RetryBackoff func return value that, if encountered, |
| // signals that the func should not be retried. In that case, the error |
| // returned by the interface method will be returned by RetryBackoff |
| type Fatal interface { |
| FatalError() error |
| } |
| |
| // NonRetryableError is a simple implementation of Fatal that wraps an error |
| type NonRetryableError struct { |
| InnerError error |
| } |
| |
| // FatalError returns the inner error, but also implements Fatal, which |
| // signals to RetryBackoff that a non-retryable error occurred. |
| func (e NonRetryableError) FatalError() error { |
| return e.InnerError |
| } |
| |
| // Error returns the inner error string |
| func (e NonRetryableError) Error() string { |
| return e.InnerError.Error() |
| } |
| |
| var ( |
| initialBackoffDelay = time.Second |
| maxBackoffDelay = 3 * time.Second |
| ) |
| |
| // RetryBackoff retries function f until nil or a FatalError is returned. |
| // RetryBackoff only returns an error if the context is in error or if a |
| // FatalError was encountered. |
| func RetryBackoff(ctx context.Context, f func() error) error { |
| // doneCh signals that the routine is done and sends the last error |
| var doneCh = make(chan struct{}) |
| var errVal atomic.Value |
| type errWrap struct { |
| E error |
| } |
| |
| go func() { |
| // the retry delay between each attempt |
| var delay time.Duration = 0 |
| defer close(doneCh) |
| |
| for { |
| select { |
| case <-ctx.Done(): |
| return |
| case <-time.After(delay): |
| } |
| |
| err := f() |
| switch e := err.(type) { |
| case nil: |
| return |
| case Fatal: |
| errVal.Store(errWrap{e.FatalError()}) |
| return |
| } |
| |
| delay *= 2 |
| if delay == 0 { |
| delay = initialBackoffDelay |
| } |
| |
| delay = min(delay, maxBackoffDelay) |
| |
| log.Printf("[WARN] retryable error: %q, delaying for %s", err, delay) |
| } |
| }() |
| |
| // Wait until done or deadline |
| select { |
| case <-doneCh: |
| case <-ctx.Done(): |
| } |
| |
| err, hadErr := errVal.Load().(errWrap) |
| var lastErr error |
| if hadErr { |
| lastErr = err.E |
| } |
| |
| if ctx.Err() != nil { |
| return ctx.Err() |
| } |
| |
| return lastErr |
| } |