-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Brandon Croft <[email protected]>
- Loading branch information
1 parent
1d13e0c
commit 35abf5c
Showing
3 changed files
with
174 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package remote | ||
|
||
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 | ||
} |