// Copyright IBM Corp. 2014, 2026 // 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 }