diff --git a/nodeadm/go.mod b/nodeadm/go.mod index 3a3de7787..405309766 100644 --- a/nodeadm/go.mod +++ b/nodeadm/go.mod @@ -27,6 +27,7 @@ require ( cel.dev/expr v0.24.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/avast/retry-go/v5 v5.0.0 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect diff --git a/nodeadm/go.sum b/nodeadm/go.sum index fe9021796..f7339d89d 100644 --- a/nodeadm/go.sum +++ b/nodeadm/go.sum @@ -6,6 +6,12 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/avast/retry-go v2.7.0+incompatible h1:XaGnzl7gESAideSjr+I8Hki/JBi+Yb9baHlMRPeSC84= +github.com/avast/retry-go v2.7.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/avast/retry-go/v5 v5.0.0 h1:kf1Qc2UsTZ4qq8elDymqfbISvkyMuhgRxuJqX2NHP7k= +github.com/avast/retry-go/v5 v5.0.0/go.mod h1://d+usmKWio1agtZfS1H/ltTqwtIfBnRq9zEwjc3eH8= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= diff --git a/nodeadm/internal/kubelet/config.go b/nodeadm/internal/kubelet/config.go index 347e7f952..d10069b07 100644 --- a/nodeadm/internal/kubelet/config.go +++ b/nodeadm/internal/kubelet/config.go @@ -19,8 +19,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8skubelet "k8s.io/kubelet/config/v1beta1" + "github.com/avast/retry-go/v5" "github.com/aws/smithy-go/ptr" - "github.com/awslabs/amazon-eks-ami/nodeadm/internal/api" "github.com/awslabs/amazon-eks-ami/nodeadm/internal/aws/imds" "github.com/awslabs/amazon-eks-ami/nodeadm/internal/containerd" @@ -178,7 +178,20 @@ func (ksc *kubeletConfig) withOutpostSetup(cfg *api.NodeConfig) error { } // TODO: cleanup - ipAddresses, err := net.LookupHost(apiUrl.Host) + var ipAddresses []string + err = retry.New( + retry.Attempts(6), + retry.Delay(200*time.Millisecond), + retry.OnRetry(func(n uint, err error) { + zap.L().Info("Retrying DNS lookup after error", zap.Error(err)) + }), + ).Do( + func() error { + var err error + ipAddresses, err = net.LookupHost(apiUrl.Host) + return err + }, + ) if err != nil { return err } diff --git a/nodeadm/vendor/github.com/avast/retry-go/v5/.gitignore b/nodeadm/vendor/github.com/avast/retry-go/v5/.gitignore new file mode 100644 index 000000000..c40eb23f9 --- /dev/null +++ b/nodeadm/vendor/github.com/avast/retry-go/v5/.gitignore @@ -0,0 +1,21 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +# dep +vendor/ +Gopkg.lock + +# cover +coverage.txt diff --git a/nodeadm/vendor/github.com/avast/retry-go/v5/.godocdown.tmpl b/nodeadm/vendor/github.com/avast/retry-go/v5/.godocdown.tmpl new file mode 100644 index 000000000..7d4b0015b --- /dev/null +++ b/nodeadm/vendor/github.com/avast/retry-go/v5/.godocdown.tmpl @@ -0,0 +1,38 @@ +# {{ .Name }} + +[![Release](https://img.shields.io/github/release/avast/retry-go.svg?style=flat-square)](https://github.com/avast/retry-go/releases/latest) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +![GitHub Actions](https://github.com/avast/retry-go/actions/workflows/workflow.yaml/badge.svg) +[![Go Report Card](https://goreportcard.com/badge/github.com/avast/retry-go?style=flat-square)](https://goreportcard.com/report/github.com/avast/retry-go) +[![Go Reference](https://pkg.go.dev/badge/github.com/avast/retry-go/v4.svg)](https://pkg.go.dev/github.com/avast/retry-go/v4) +[![codecov.io](https://codecov.io/github/avast/retry-go/coverage.svg?branch=main)](https://codecov.io/github/avast/retry-go?branch=main) +[![Sourcegraph](https://sourcegraph.com/github.com/avast/retry-go/-/badge.svg)](https://sourcegraph.com/github.com/avast/retry-go?badge) + +{{ .EmitSynopsis }} + +{{ .EmitUsage }} + +## Contributing + +Contributions are very much welcome. + +### Makefile + +Makefile provides several handy rules, like README.md `generator` , `setup` for prepare build/dev environment, `test`, `cover`, etc... + +Try `make help` for more information. + +### Before pull request + +> maybe you need `make setup` in order to setup environment + +please try: +* run tests (`make test`) +* run linter (`make lint`) +* if your IDE don't automaticaly do `go fmt`, run `go fmt` (`make fmt`) + +### README + +README.md are generate from template [.godocdown.tmpl](.godocdown.tmpl) and code documentation via [godocdown](https://github.com/robertkrimen/godocdown). + +Never edit README.md direct, because your change will be lost. diff --git a/nodeadm/vendor/github.com/avast/retry-go/v5/LICENSE b/nodeadm/vendor/github.com/avast/retry-go/v5/LICENSE new file mode 100644 index 000000000..f63fca814 --- /dev/null +++ b/nodeadm/vendor/github.com/avast/retry-go/v5/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Avast + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/nodeadm/vendor/github.com/avast/retry-go/v5/Makefile b/nodeadm/vendor/github.com/avast/retry-go/v5/Makefile new file mode 100644 index 000000000..27e4503a4 --- /dev/null +++ b/nodeadm/vendor/github.com/avast/retry-go/v5/Makefile @@ -0,0 +1,54 @@ +SOURCE_FILES?=$$(go list ./... | grep -v /vendor/) +TEST_PATTERN?=. +TEST_OPTIONS?= +VERSION?=$$(cat VERSION) +LINTER?=$$(which golangci-lint) +LINTER_VERSION=2.5.0 + +ifeq ($(OS),Windows_NT) + LINTER_FILE=golangci-lint-$(LINTER_VERSION)-windows-amd64.zip + LINTER_UNPACK= >| app.zip; unzip -j app.zip -d $$GOPATH/bin; rm app.zip +else ifeq ($(OS), Darwin) + LINTER_FILE=golangci-lint-$(LINTER_VERSION)-darwin-amd64.tar.gz + LINTER_UNPACK= | tar xzf - -C $$GOPATH/bin --wildcards --strip 1 "**/golangci-lint" +else + LINTER_FILE=golangci-lint-$(LINTER_VERSION)-linux-amd64.tar.gz + LINTER_UNPACK= | tar xzf - -C $$GOPATH/bin --wildcards --strip 1 "**/golangci-lint" +endif + +setup: + go install github.com/pierrre/gotestcover@latest + go install golang.org/x/tools/cmd/cover@latest + go install github.com/robertkrimen/godocdown/godocdown@latest + go mod download + +generate: ## Generate README.md + godocdown >| README.md + +test: generate test_and_cover_report lint + +test_and_cover_report: + gotestcover $(TEST_OPTIONS) -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m + +cover: test ## Run all the tests and opens the coverage report + go tool cover -html=coverage.txt + +fmt: ## gofmt and goimports all go files + find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done + +lint: ## Run all the linters + docker run --rm -v $$(pwd):/app -w /app golangci/golangci-lint:v$(LINTER_VERSION) golangci-lint run + +ci: test_and_cover_report ## Run all the tests but no linters - use https://golangci.com integration instead + +build: + go build + +release: ## Release new version + git tag | grep -q $(VERSION) && echo This version was released! Increase VERSION! || git tag $(VERSION) && git push origin $(VERSION) && git tag v$(VERSION) && git push origin v$(VERSION) + +# Absolutely awesome: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.DEFAULT_GOAL := build diff --git a/nodeadm/vendor/github.com/avast/retry-go/v5/README.md b/nodeadm/vendor/github.com/avast/retry-go/v5/README.md new file mode 100644 index 000000000..d419bbe39 --- /dev/null +++ b/nodeadm/vendor/github.com/avast/retry-go/v5/README.md @@ -0,0 +1,687 @@ +# retry + +[![Release](https://img.shields.io/github/release/avast/retry-go.svg?style=flat-square)](https://github.com/avast/retry-go/releases/latest) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +![GitHub Actions](https://github.com/avast/retry-go/actions/workflows/workflow.yaml/badge.svg) +[![Go Report Card](https://goreportcard.com/badge/github.com/avast/retry-go?style=flat-square)](https://goreportcard.com/report/github.com/avast/retry-go) +[![Go Reference](https://pkg.go.dev/badge/github.com/avast/retry-go/v4.svg)](https://pkg.go.dev/github.com/avast/retry-go/v4) +[![codecov.io](https://codecov.io/github/avast/retry-go/coverage.svg?branch=main)](https://codecov.io/github/avast/retry-go?branch=main) +[![Sourcegraph](https://sourcegraph.com/github.com/avast/retry-go/-/badge.svg)](https://sourcegraph.com/github.com/avast/retry-go?badge) + +Simple library for retry mechanism + +Slightly inspired by +[Try::Tiny::Retry](https://metacpan.org/pod/Try::Tiny::Retry) + +# SYNOPSIS + +HTTP GET with retry: + + url := "http://example.com" + var body []byte + + err := retry.New( + retry.Attempts(5), + retry.Delay(100*time.Millisecond), + ).Do( + func() error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return nil + }, + ) + + if err != nil { + // handle error + } + + fmt.Println(string(body)) + +HTTP GET with retry with data: + + url := "http://example.com" + + body, err := retry.DoWithData(retry.New(), + func() ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil + }, + ) + + if err != nil { + // handle error + } + + fmt.Println(string(body)) + +Reusable retrier for high-frequency retry operations: + + // Create retrier once, reuse many times + retrier := retry.New( + retry.Attempts(5), + retry.Delay(100*time.Millisecond), + ) + + // Minimal allocations in happy path + for { + err := retrier.Do( + func() error { + return doWork() + }, + ) + if err != nil { + // handle error + } + } + +[More examples](https://github.com/avast/retry-go/tree/main/examples) + +# SEE ALSO + +* [codeGROOVE-dev/retry](https://github.com/codeGROOVE-dev/retry) - Modern fork +of avast/retry-go/v4 focused on correctness, reliability and efficiency. 100% +API-compatible drop-in replacement. Looks really good. + +* [giantswarm/retry-go](https://github.com/giantswarm/retry-go) - slightly +complicated interface. + +* [sethgrid/pester](https://github.com/sethgrid/pester) - only http retry for +http calls with retries and backoff + +* [cenkalti/backoff](https://github.com/cenkalti/backoff) - Go port of the +exponential backoff algorithm from Google's HTTP Client Library for Java. Really +complicated interface. + +* [rafaeljesus/retry-go](https://github.com/rafaeljesus/retry-go) - looks good, +slightly similar as this package, don't have 'simple' `Retry` method + +* [matryer/try](https://github.com/matryer/try) - very popular package, +nonintuitive interface (for me) + +# BREAKING CHANGES + +* 5.0.0 + + - Complete API redesign: method-based retry operations + - Renamed `Config` type to `Retrier` + - Renamed `NewConfig()` to `New()` + - Changed from package-level functions to methods: `retry.Do(func, config)` → `retry.New(opts...).Do(func)` + - `DelayTypeFunc` signature changed: `func(n uint, err error, config *Config)` → `func(n uint, err error, r *Retrier)` + - Migration: `retry.Do(func, opts...)` → `retry.New(opts...).Do(func)` (simple find & replace) + - This change improves performance, simplifies the API, and provides a cleaner interface + - `Unwrap()` now returns `[]error` instead of `error` to support Go 1.20 multiple error wrapping. + - `errors.Unwrap(err)` will now return `nil` (same as `errors.Join`). Use `errors.Is` or `errors.As` to inspect wrapped errors. + +* 4.0.0 + + - infinity retry is possible by set `Attempts(0)` by PR [#49](https://github.com/avast/retry-go/pull/49) + +* 3.0.0 + + - `DelayTypeFunc` accepts a new parameter `err` - this breaking change affects only your custom Delay Functions. This change allow [make delay functions based on error](examples/delay_based_on_error_test.go). + +* 1.0.2 -> 2.0.0 + + - argument of `retry.Delay` is final delay (no multiplication by `retry.Units` anymore) + - function `retry.Units` are removed + - [more about this breaking change](https://github.com/avast/retry-go/issues/7) + +* 0.3.0 -> 1.0.0 + + - `retry.Retry` function are changed to `retry.Do` function + - `retry.RetryCustom` (OnRetry) and `retry.RetryCustomWithOpts` functions are now implement via functions produces Options (aka `retry.OnRetry`) + +## Usage + +#### func BackOffDelay + +```go +func BackOffDelay(n uint, _ error, config DelayContext) time.Duration +``` +BackOffDelay is a DelayType which increases delay between consecutive retries + +#### func FixedDelay + +```go +func FixedDelay(_ uint, _ error, config DelayContext) time.Duration +``` +FixedDelay is a DelayType which keeps delay the same through all iterations + +#### func FullJitterBackoffDelay + +```go +func FullJitterBackoffDelay(n uint, err error, config DelayContext) time.Duration +``` +FullJitterBackoffDelay is a DelayTypeFunc that calculates delay using +exponential backoff with full jitter. The delay is a random value between 0 and +the current backoff ceiling. Formula: sleep = random_between(0, min(cap, base * +2^attempt)) It uses config.Delay as the base delay and config.MaxDelay as the +cap. + +#### func IsRecoverable + +```go +func IsRecoverable(err error) bool +``` +IsRecoverable checks if error is an instance of `unrecoverableError` + +#### func RandomDelay + +```go +func RandomDelay(_ uint, _ error, config DelayContext) time.Duration +``` +RandomDelay is a DelayType which picks a random delay up to maxJitter + +#### func Unrecoverable + +```go +func Unrecoverable(err error) error +``` +Unrecoverable wraps an error in `unrecoverableError` struct + +#### type DelayContext + +```go +type DelayContext interface { + Delay() time.Duration + MaxJitter() time.Duration + MaxBackOffN() uint + MaxDelay() time.Duration +} +``` + +DelayContext provides configuration values needed for delay calculation. + +#### type DelayTypeFunc + +```go +type DelayTypeFunc func(n uint, err error, config DelayContext) time.Duration +``` + +DelayTypeFunc is called to return the next delay to wait after the retriable +function fails on `err` after `n` attempts. + +#### func CombineDelay + +```go +func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc +``` +CombineDelay is a DelayType the combines all of the specified delays into a new +DelayTypeFunc + +#### type Error + +```go +type Error []error +``` + +Error type represents list of errors in retry + +#### func (Error) As + +```go +func (e Error) As(target interface{}) bool +``` + +#### func (Error) Error + +```go +func (e Error) Error() string +``` +Error method return string representation of Error It is an implementation of +error interface + +#### func (Error) Is + +```go +func (e Error) Is(target error) bool +``` + +#### func (Error) LastError + +```go +func (e Error) LastError() error +``` +LastError returns the last error in the error list. + +This is a convenience method for users migrating from retry-go v4.x where +errors.Unwrap(err) returned the last error. In v5.0.0, errors.Unwrap(err) +returns nil due to the switch to Unwrap() []error for Go 1.20 compatibility. + +Migration example: + + // v4.x code: + lastErr := errors.Unwrap(retryErr) + + // v5.0.0 code (option 1 - recommended): + if errors.Is(retryErr, specificError) { ... } + + // v5.0.0 code (option 2 - if you need the last error): + lastErr := retryErr.(retry.Error).LastError() + +Note: Using errors.Is or errors.As is preferred as they check ALL wrapped +errors, not just the last one. + +#### func (Error) Unwrap + +```go +func (e Error) Unwrap() []error +``` +Unwrap returns the list of errors that this Error is wrapping. + +This method implements the Unwrap() []error interface introduced in Go 1.20 for +multi-error unwrapping. This allows errors.Is and errors.As to traverse all +wrapped errors, not just the last one. + +IMPORTANT: errors.Unwrap(err) will return nil because the standard library's +errors.Unwrap function only calls Unwrap() error, not Unwrap() []error. This is +the same behavior as errors.Join in Go 1.20. + +Example - Use errors.Is to check for specific errors: + + err := retry.New(retry.Attempts(3)).Do(func() error { + return os.ErrNotExist + }) + if errors.Is(err, os.ErrNotExist) { + // Handle not exist error + } + +Example - Use errors.As to extract error details: + + var pathErr *fs.PathError + if errors.As(err, &pathErr) { + fmt.Println("Failed at path:", pathErr.Path) + } + +Example - Get the last error directly (for migration): + + if retryErr, ok := err.(retry.Error); ok { + lastErr := retryErr.LastError() + } + +See also: LastError() for direct access to the last error. + +#### func (Error) WrappedErrors + +```go +func (e Error) WrappedErrors() []error +``` +WrappedErrors returns the list of errors that this Error is wrapping. It is an +implementation of the `errwrap.Wrapper` interface in package +[errwrap](https://github.com/hashicorp/errwrap) so that `retry.Error` can be +used with that library. + +#### type OnRetryFunc + +```go +type OnRetryFunc func(attempt uint, err error) +``` + +Function signature of OnRetry function + +#### type Option + +```go +type Option func(*retrierCore) +``` + +Option represents an option for retry. + +#### func Attempts + +```go +func Attempts(attempts uint) Option +``` +Attempts set count of retry. Setting to 0 will retry until the retried function +succeeds. default is 10 + +#### func AttemptsForError + +```go +func AttemptsForError(attempts uint, err error) Option +``` +AttemptsForError sets count of retry in case execution results in given `err` +Retries for the given `err` are also counted against total retries. The retry +will stop if any of given retries is exhausted. + +added in 4.3.0 + +#### func Context + +```go +func Context(ctx context.Context) Option +``` +Context allow to set context of retry default are Background context + +example of immediately cancellation (maybe it isn't the best example, but it +describes behavior enough; I hope) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + retry.New().Do( + func() error { + ... + }, + retry.Context(ctx), + ) + +#### func Delay + +```go +func Delay(delay time.Duration) Option +``` +Delay set delay between retry default is 100ms + +#### func DelayType + +```go +func DelayType(delayType DelayTypeFunc) Option +``` +DelayType set type of the delay between retries default is a combination of +BackOffDelay and RandomDelay for exponential backoff with jitter + +#### func LastErrorOnly + +```go +func LastErrorOnly(lastErrorOnly bool) Option +``` +return the direct last error that came from the retried function default is +false (return wrapped errors with everything) + +#### func MaxDelay + +```go +func MaxDelay(maxDelay time.Duration) Option +``` +MaxDelay set maximum delay between retry does not apply by default + +#### func MaxJitter + +```go +func MaxJitter(maxJitter time.Duration) Option +``` +MaxJitter sets the maximum random Jitter between retries for RandomDelay + +#### func OnRetry + +```go +func OnRetry(onRetry OnRetryFunc) Option +``` +OnRetry function callback are called each retry + +log each retry example: + + retry.New().Do( + func() error { + return errors.New("some error") + }, + retry.OnRetry(func(n uint, err error) { + log.Printf("#%d: %s\n", n, err) + }), + ) + +#### func RetryIf + +```go +func RetryIf(retryIf RetryIfFunc) Option +``` +RetryIf controls whether a retry should be attempted after an error (assuming +there are any retry attempts remaining) + +skip retry if special error example: + + retry.New().Do( + func() error { + return errors.New("special error") + }, + retry.RetryIf(func(err error) bool { + if err.Error() == "special error" { + return false + } + return true + }) + ) + +By default RetryIf stops execution if the error is wrapped using +`retry.Unrecoverable`, so above example may also be shortened to: + + retry.New().Do( + func() error { + return retry.Unrecoverable(errors.New("special error")) + } + ) + +#### func UntilSucceeded + +```go +func UntilSucceeded() Option +``` +UntilSucceeded will retry until the retried function succeeds. Equivalent to +setting Attempts(0). + +#### func WithTimer + +```go +func WithTimer(t Timer) Option +``` +WithTimer provides a way to swap out timer module implementations. This +primarily is useful for mocking/testing, where you may not want to explicitly +wait for a set duration for retries. + +example of augmenting time.After with a print statement + + type struct MyTimer {} + + func (t *MyTimer) After(d time.Duration) <- chan time.Time { + fmt.Print("Timer called!") + return time.After(d) + } + + retry.New().Do( + func() error { ... }, + retry.WithTimer(&MyTimer{}) + ) + +#### func WrapContextErrorWithLastError + +```go +func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option +``` +WrapContextErrorWithLastError allows the context error to be returned wrapped +with the last error that the retried function returned. This is only applicable +when Attempts is set to 0 to retry indefinitly and when using a context to +cancel / timeout + +default is false + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + retry.New().Do( + func() error { + ... + }, + retry.Context(ctx), + retry.Attempts(0), + retry.WrapContextErrorWithLastError(true), + ) + +#### type Retrier + +```go +type Retrier struct { +} +``` + +Retrier is for retry operations that return only an error. + +#### func New + +```go +func New(opts ...Option) *Retrier +``` +New creates a new Retrier with the given options. The returned Retrier can be +safely reused across multiple retry operations. + +#### func (Retrier) Delay + +```go +func (r Retrier) Delay() time.Duration +``` +Delay implements DelayContext + +#### func (*Retrier) Do + +```go +func (r *Retrier) Do(retryableFunc RetryableFunc) error +``` +Do executes the retryable function using this Retrier's configuration. + +#### func (Retrier) MaxBackOffN + +```go +func (r Retrier) MaxBackOffN() uint +``` +MaxBackOffN implements DelayContext + +#### func (Retrier) MaxDelay + +```go +func (r Retrier) MaxDelay() time.Duration +``` +MaxDelay implements DelayContext + +#### func (Retrier) MaxJitter + +```go +func (r Retrier) MaxJitter() time.Duration +``` +MaxJitter implements DelayContext + +#### type RetrierWithData + +```go +type RetrierWithData[T any] struct { +} +``` + +RetrierWithData is for retry operations that return data and an error. + +#### func NewWithData + +```go +func NewWithData[T any](opts ...Option) *RetrierWithData[T] +``` +NewWithData creates a new RetrierWithData[T] with the given options. The +returned retrier can be safely reused across multiple retry operations. + +#### func (RetrierWithData) Delay + +```go +func (r RetrierWithData) Delay() time.Duration +``` +Delay implements DelayContext + +#### func (*RetrierWithData[T]) Do + +```go +func (r *RetrierWithData[T]) Do(retryableFunc RetryableFuncWithData[T]) (T, error) +``` +Do executes the retryable function using this RetrierWithData's configuration. + +#### func (RetrierWithData) MaxBackOffN + +```go +func (r RetrierWithData) MaxBackOffN() uint +``` +MaxBackOffN implements DelayContext + +#### func (RetrierWithData) MaxDelay + +```go +func (r RetrierWithData) MaxDelay() time.Duration +``` +MaxDelay implements DelayContext + +#### func (RetrierWithData) MaxJitter + +```go +func (r RetrierWithData) MaxJitter() time.Duration +``` +MaxJitter implements DelayContext + +#### type RetryIfFunc + +```go +type RetryIfFunc func(error) bool +``` + +Function signature of retry if function + +#### type RetryableFunc + +```go +type RetryableFunc func() error +``` + +Function signature of retryable function + +#### type RetryableFuncWithData + +```go +type RetryableFuncWithData[T any] func() (T, error) +``` + +Function signature of retryable function with data + +#### type Timer + +```go +type Timer interface { + After(time.Duration) <-chan time.Time +} +``` + +Timer represents the timer used to track time for a retry. + +## Contributing + +Contributions are very much welcome. + +### Makefile + +Makefile provides several handy rules, like README.md `generator` , `setup` for prepare build/dev environment, `test`, `cover`, etc... + +Try `make help` for more information. + +### Before pull request + +> maybe you need `make setup` in order to setup environment + +please try: +* run tests (`make test`) +* run linter (`make lint`) +* if your IDE don't automaticaly do `go fmt`, run `go fmt` (`make fmt`) + +### README + +README.md are generate from template [.godocdown.tmpl](.godocdown.tmpl) and code documentation via [godocdown](https://github.com/robertkrimen/godocdown). + +Never edit README.md direct, because your change will be lost. diff --git a/nodeadm/vendor/github.com/avast/retry-go/v5/VERSION b/nodeadm/vendor/github.com/avast/retry-go/v5/VERSION new file mode 100644 index 000000000..0062ac971 --- /dev/null +++ b/nodeadm/vendor/github.com/avast/retry-go/v5/VERSION @@ -0,0 +1 @@ +5.0.0 diff --git a/nodeadm/vendor/github.com/avast/retry-go/v5/current.txt b/nodeadm/vendor/github.com/avast/retry-go/v5/current.txt new file mode 100644 index 000000000..406b14fe8 --- /dev/null +++ b/nodeadm/vendor/github.com/avast/retry-go/v5/current.txt @@ -0,0 +1,26 @@ +goos: darwin +goarch: amd64 +pkg: github.com/avast/retry-go/v4 +cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz +BenchmarkDo-16 3 474128987 ns/op 2730 B/op 48 allocs/op +BenchmarkDo-16 3 441499631 ns/op 2725 B/op 47 allocs/op +BenchmarkDo-16 3 449390845 ns/op 2693 B/op 47 allocs/op +BenchmarkDo-16 3 488695333 ns/op 2725 B/op 47 allocs/op +BenchmarkDo-16 2 601685067 ns/op 2704 B/op 48 allocs/op +BenchmarkDo-16 3 336872997 ns/op 2693 B/op 47 allocs/op +BenchmarkDo-16 3 384347911 ns/op 2725 B/op 47 allocs/op +BenchmarkDo-16 3 480906307 ns/op 2693 B/op 47 allocs/op +BenchmarkDo-16 3 455362447 ns/op 2693 B/op 47 allocs/op +BenchmarkDo-16 3 443170384 ns/op 2693 B/op 47 allocs/op +BenchmarkDoNoErrors-16 6872852 159.4 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7650360 161.3 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7235683 159.3 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7465636 160.2 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7549692 160.7 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7510610 159.8 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7438124 160.3 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7416504 160.2 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7356183 160.4 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7393480 160.1 ns/op 208 B/op 4 allocs/op +PASS +ok github.com/avast/retry-go/v4 35.971s diff --git a/nodeadm/vendor/github.com/avast/retry-go/v5/generic.txt b/nodeadm/vendor/github.com/avast/retry-go/v5/generic.txt new file mode 100644 index 000000000..116a09645 --- /dev/null +++ b/nodeadm/vendor/github.com/avast/retry-go/v5/generic.txt @@ -0,0 +1,46 @@ +goos: darwin +goarch: amd64 +pkg: github.com/avast/retry-go/v4 +cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz +BenchmarkDo-16 3 406306609 ns/op 2701 B/op 48 allocs/op +BenchmarkDo-16 3 419470846 ns/op 2693 B/op 47 allocs/op +BenchmarkDo-16 2 567716303 ns/op 2696 B/op 47 allocs/op +BenchmarkDo-16 2 562713288 ns/op 2696 B/op 47 allocs/op +BenchmarkDo-16 3 418301987 ns/op 2693 B/op 47 allocs/op +BenchmarkDo-16 2 541207332 ns/op 2696 B/op 47 allocs/op +BenchmarkDo-16 2 526211617 ns/op 2696 B/op 47 allocs/op +BenchmarkDo-16 2 517419526 ns/op 2696 B/op 47 allocs/op +BenchmarkDo-16 3 478391497 ns/op 2693 B/op 47 allocs/op +BenchmarkDo-16 3 452548175 ns/op 2725 B/op 47 allocs/op +BenchmarkDoWithData-16 3 463040866 ns/op 2693 B/op 47 allocs/op +BenchmarkDoWithData-16 3 496158943 ns/op 2693 B/op 47 allocs/op +BenchmarkDoWithData-16 3 488367012 ns/op 2725 B/op 47 allocs/op +BenchmarkDoWithData-16 3 454618897 ns/op 2693 B/op 47 allocs/op +BenchmarkDoWithData-16 3 435430056 ns/op 2693 B/op 47 allocs/op +BenchmarkDoWithData-16 2 552289967 ns/op 2744 B/op 48 allocs/op +BenchmarkDoWithData-16 3 569748815 ns/op 2693 B/op 47 allocs/op +BenchmarkDoWithData-16 3 416597207 ns/op 2725 B/op 47 allocs/op +BenchmarkDoWithData-16 3 358455415 ns/op 2725 B/op 47 allocs/op +BenchmarkDoWithData-16 3 455297803 ns/op 2725 B/op 47 allocs/op +BenchmarkDoNoErrors-16 7035135 161.9 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7389806 161.3 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7394016 161.5 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7380039 162.2 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7424865 162.2 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7111860 160.5 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7285305 162.6 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7410627 160.7 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7340961 161.6 ns/op 208 B/op 4 allocs/op +BenchmarkDoNoErrors-16 7295727 164.1 ns/op 208 B/op 4 allocs/op +BenchmarkDoWithDataNoErrors-16 7357304 159.9 ns/op 208 B/op 4 allocs/op +BenchmarkDoWithDataNoErrors-16 6649852 166.9 ns/op 208 B/op 4 allocs/op +BenchmarkDoWithDataNoErrors-16 6938404 176.3 ns/op 208 B/op 4 allocs/op +BenchmarkDoWithDataNoErrors-16 7181965 160.4 ns/op 208 B/op 4 allocs/op +BenchmarkDoWithDataNoErrors-16 7311484 166.2 ns/op 208 B/op 4 allocs/op +BenchmarkDoWithDataNoErrors-16 6939157 169.7 ns/op 208 B/op 4 allocs/op +BenchmarkDoWithDataNoErrors-16 6648344 179.0 ns/op 208 B/op 4 allocs/op +BenchmarkDoWithDataNoErrors-16 6794847 177.0 ns/op 208 B/op 4 allocs/op +BenchmarkDoWithDataNoErrors-16 6782588 171.4 ns/op 208 B/op 4 allocs/op +BenchmarkDoWithDataNoErrors-16 7279119 166.9 ns/op 208 B/op 4 allocs/op +PASS +ok github.com/avast/retry-go/v4 73.128s diff --git a/nodeadm/vendor/github.com/avast/retry-go/v5/options.go b/nodeadm/vendor/github.com/avast/retry-go/v5/options.go new file mode 100644 index 000000000..4dca20d9b --- /dev/null +++ b/nodeadm/vendor/github.com/avast/retry-go/v5/options.go @@ -0,0 +1,384 @@ +package retry + +import ( + "context" + "math" + "math/rand" + "time" +) + +// Function signature of retry if function +type RetryIfFunc func(error) bool + +// Function signature of OnRetry function +type OnRetryFunc func(attempt uint, err error) + +// DelayContext provides configuration values needed for delay calculation. +type DelayContext interface { + Delay() time.Duration + MaxJitter() time.Duration + MaxBackOffN() uint + MaxDelay() time.Duration +} + +// DelayTypeFunc is called to return the next delay to wait after the retriable function fails on `err` after `n` attempts. +type DelayTypeFunc func(n uint, err error, config DelayContext) time.Duration + +// Timer represents the timer used to track time for a retry. +type Timer interface { + After(time.Duration) <-chan time.Time +} + +// retrierCore holds the core configuration and business logic for retry operations. +// this is then used by Retrier and RetrierWithData public APIs +type retrierCore struct { + attempts uint + attemptsForError map[error]uint + delay time.Duration + maxDelay time.Duration + maxJitter time.Duration + onRetry OnRetryFunc + retryIf RetryIfFunc + delayType DelayTypeFunc + lastErrorOnly bool + context context.Context + timer Timer + wrapContextErrorWithLastError bool + + maxBackOffN uint // pre-computed for BackOffDelay, immutable after New() +} + +// Delay implements DelayContext +func (r *retrierCore) Delay() time.Duration { + return r.delay +} + +// MaxJitter implements DelayContext +func (r *retrierCore) MaxJitter() time.Duration { + return r.maxJitter +} + +// MaxBackOffN implements DelayContext +func (r *retrierCore) MaxBackOffN() uint { + return r.maxBackOffN +} + +// MaxDelay implements DelayContext +func (r *retrierCore) MaxDelay() time.Duration { + return r.maxDelay +} + +// Retrier is for retry operations that return only an error. +type Retrier struct { + *retrierCore +} + +// RetrierWithData is for retry operations that return data and an error. +type RetrierWithData[T any] struct { + *retrierCore +} + +// Option represents an option for retry. +type Option func(*retrierCore) + +func newRetrieerCore(opts ...Option) *retrierCore { + core := &retrierCore{ + attempts: uint(10), + attemptsForError: make(map[error]uint), + delay: 100 * time.Millisecond, + maxJitter: 100 * time.Millisecond, + onRetry: func(n uint, err error) {}, + retryIf: IsRecoverable, + delayType: CombineDelay(BackOffDelay, RandomDelay), + lastErrorOnly: false, + context: context.Background(), + timer: &timerImpl{}, + } + + for _, opt := range opts { + opt(core) + } + + const maxBackOffN uint = 62 + core.maxBackOffN = maxBackOffN + if core.delay < 0 { + core.delay = 0 + } else if core.delay > 0 { + core.maxBackOffN = maxBackOffN - uint(math.Floor(math.Log2(float64(core.delay)))) + } + + return core +} + +// New creates a new Retrier with the given options. +// The returned Retrier can be safely reused across multiple retry operations. +func New(opts ...Option) *Retrier { + return &Retrier{retrierCore: newRetrieerCore(opts...)} +} + +// NewWithData creates a new RetrierWithData[T] with the given options. +// The returned retrier can be safely reused across multiple retry operations. +func NewWithData[T any](opts ...Option) *RetrierWithData[T] { + return &RetrierWithData[T]{retrierCore: newRetrieerCore(opts...)} +} + +func emptyOption(r *retrierCore) {} + +// return the direct last error that came from the retried function +// default is false (return wrapped errors with everything) +func LastErrorOnly(lastErrorOnly bool) Option { + return func(r *retrierCore) { + r.lastErrorOnly = lastErrorOnly + } +} + +// Attempts set count of retry. Setting to 0 will retry until the retried function succeeds. +// default is 10 +func Attempts(attempts uint) Option { + return func(r *retrierCore) { + r.attempts = attempts + } +} + +// UntilSucceeded will retry until the retried function succeeds. Equivalent to setting Attempts(0). +func UntilSucceeded() Option { + return func(r *retrierCore) { + r.attempts = 0 + } +} + +// AttemptsForError sets count of retry in case execution results in given `err` +// Retries for the given `err` are also counted against total retries. +// The retry will stop if any of given retries is exhausted. +// +// added in 4.3.0 +func AttemptsForError(attempts uint, err error) Option { + return func(r *retrierCore) { + r.attemptsForError[err] = attempts + } +} + +// Delay set delay between retry +// default is 100ms +func Delay(delay time.Duration) Option { + return func(r *retrierCore) { + r.delay = delay + } +} + +// MaxDelay set maximum delay between retry +// does not apply by default +func MaxDelay(maxDelay time.Duration) Option { + return func(r *retrierCore) { + r.maxDelay = maxDelay + } +} + +// MaxJitter sets the maximum random Jitter between retries for RandomDelay +func MaxJitter(maxJitter time.Duration) Option { + return func(r *retrierCore) { + r.maxJitter = maxJitter + } +} + +// DelayType set type of the delay between retries +// default is a combination of BackOffDelay and RandomDelay for exponential backoff with jitter +func DelayType(delayType DelayTypeFunc) Option { + if delayType == nil { + return emptyOption + } + return func(r *retrierCore) { + r.delayType = delayType + } +} + +// BackOffDelay is a DelayType which increases delay between consecutive retries +func BackOffDelay(n uint, _ error, config DelayContext) time.Duration { + maxBackOffN := config.MaxBackOffN() + n-- + if n > maxBackOffN { + n = maxBackOffN + } + return config.Delay() << n +} + +// FixedDelay is a DelayType which keeps delay the same through all iterations +func FixedDelay(_ uint, _ error, config DelayContext) time.Duration { + return config.Delay() +} + +// RandomDelay is a DelayType which picks a random delay up to maxJitter +func RandomDelay(_ uint, _ error, config DelayContext) time.Duration { + maxJitter := config.MaxJitter() + if maxJitter == 0 { + return 0 + } + return time.Duration(rand.Int63n(int64(maxJitter))) +} + +// CombineDelay is a DelayType the combines all of the specified delays into a new DelayTypeFunc +func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc { + const maxInt64 = uint64(math.MaxInt64) + + return func(n uint, err error, config DelayContext) time.Duration { + var total uint64 + for _, delay := range delays { + total += uint64(delay(n, err, config)) + if total > maxInt64 { + total = maxInt64 + } + } + + return time.Duration(total) + } +} + +// FullJitterBackoffDelay is a DelayTypeFunc that calculates delay using exponential backoff +// with full jitter. The delay is a random value between 0 and the current backoff ceiling. +// Formula: sleep = random_between(0, min(cap, base * 2^attempt)) +// It uses config.Delay as the base delay and config.MaxDelay as the cap. +func FullJitterBackoffDelay(n uint, err error, config DelayContext) time.Duration { + // Calculate the exponential backoff ceiling for the current attempt + backoffCeiling := float64(config.Delay()) * math.Pow(2, float64(n)) + currentCap := float64(config.MaxDelay()) + + // If MaxDelay is set and backoffCeiling exceeds it, cap at MaxDelay + if currentCap > 0 && backoffCeiling > currentCap { + backoffCeiling = currentCap + } + + // Ensure backoffCeiling is at least 0 + if backoffCeiling < 0 { + backoffCeiling = 0 + } + + // Add jitter: random value between 0 and backoffCeiling + // rand.Int63n panics if argument is <= 0 + if backoffCeiling <= 0 { + return 0 // No delay if ceiling is zero or negative + } + + jitter := rand.Int63n(int64(backoffCeiling)) // #nosec G404 -- Using math/rand is acceptable for non-security critical jitter. + return time.Duration(jitter) +} + +// OnRetry function callback are called each retry +// +// log each retry example: +// +// retry.New().Do( +// func() error { +// return errors.New("some error") +// }, +// retry.OnRetry(func(n uint, err error) { +// log.Printf("#%d: %s\n", n, err) +// }), +// ) +func OnRetry(onRetry OnRetryFunc) Option { + if onRetry == nil { + return emptyOption + } + return func(r *retrierCore) { + r.onRetry = onRetry + } +} + +// RetryIf controls whether a retry should be attempted after an error +// (assuming there are any retry attempts remaining) +// +// skip retry if special error example: +// +// retry.New().Do( +// func() error { +// return errors.New("special error") +// }, +// retry.RetryIf(func(err error) bool { +// if err.Error() == "special error" { +// return false +// } +// return true +// }) +// ) +// +// By default RetryIf stops execution if the error is wrapped using `retry.Unrecoverable`, +// so above example may also be shortened to: +// +// retry.New().Do( +// func() error { +// return retry.Unrecoverable(errors.New("special error")) +// } +// ) +func RetryIf(retryIf RetryIfFunc) Option { + if retryIf == nil { + return emptyOption + } + return func(r *retrierCore) { + r.retryIf = retryIf + } +} + +// Context allow to set context of retry +// default are Background context +// +// example of immediately cancellation (maybe it isn't the best example, but it describes behavior enough; I hope) +// +// ctx, cancel := context.WithCancel(context.Background()) +// cancel() +// +// retry.New().Do( +// func() error { +// ... +// }, +// retry.Context(ctx), +// ) +func Context(ctx context.Context) Option { + return func(r *retrierCore) { + r.context = ctx + } +} + +// WithTimer provides a way to swap out timer module implementations. +// This primarily is useful for mocking/testing, where you may not want to explicitly wait for a set duration +// for retries. +// +// example of augmenting time.After with a print statement +// +// type struct MyTimer {} +// +// func (t *MyTimer) After(d time.Duration) <- chan time.Time { +// fmt.Print("Timer called!") +// return time.After(d) +// } +// +// retry.New().Do( +// func() error { ... }, +// retry.WithTimer(&MyTimer{}) +// ) +func WithTimer(t Timer) Option { + return func(r *retrierCore) { + r.timer = t + } +} + +// WrapContextErrorWithLastError allows the context error to be returned wrapped with the last error that the +// retried function returned. This is only applicable when Attempts is set to 0 to retry indefinitly and when +// using a context to cancel / timeout +// +// default is false +// +// ctx, cancel := context.WithCancel(context.Background()) +// defer cancel() +// +// retry.New().Do( +// func() error { +// ... +// }, +// retry.Context(ctx), +// retry.Attempts(0), +// retry.WrapContextErrorWithLastError(true), +// ) +func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option { + return func(r *retrierCore) { + r.wrapContextErrorWithLastError = wrapContextErrorWithLastError + } +} diff --git a/nodeadm/vendor/github.com/avast/retry-go/v5/retry.go b/nodeadm/vendor/github.com/avast/retry-go/v5/retry.go new file mode 100644 index 000000000..7d6cb9304 --- /dev/null +++ b/nodeadm/vendor/github.com/avast/retry-go/v5/retry.go @@ -0,0 +1,409 @@ +/* +Simple library for retry mechanism + +Slightly inspired by [Try::Tiny::Retry](https://metacpan.org/pod/Try::Tiny::Retry) + +# SYNOPSIS + +HTTP GET with retry: + + url := "http://example.com" + var body []byte + + err := retry.New( + retry.Attempts(5), + retry.Delay(100*time.Millisecond), + ).Do( + func() error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return nil + }, + ) + + if err != nil { + // handle error + } + + fmt.Println(string(body)) + +HTTP GET with retry with data: + + url := "http://example.com" + + body, err := retry.DoWithData(retry.New(), + func() ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil + }, + ) + + if err != nil { + // handle error + } + + fmt.Println(string(body)) + +Reusable retrier for high-frequency retry operations: + + // Create retrier once, reuse many times + retrier := retry.New( + retry.Attempts(5), + retry.Delay(100*time.Millisecond), + ) + + // Minimal allocations in happy path + for { + err := retrier.Do( + func() error { + return doWork() + }, + ) + if err != nil { + // handle error + } + } + +[More examples](https://github.com/avast/retry-go/tree/main/examples) + +# SEE ALSO + +* [codeGROOVE-dev/retry](https://github.com/codeGROOVE-dev/retry) - Modern fork of avast/retry-go/v4 focused on correctness, reliability and efficiency. 100% API-compatible drop-in replacement. Looks really good. + +* [giantswarm/retry-go](https://github.com/giantswarm/retry-go) - slightly complicated interface. + +* [sethgrid/pester](https://github.com/sethgrid/pester) - only http retry for http calls with retries and backoff + +* [cenkalti/backoff](https://github.com/cenkalti/backoff) - Go port of the exponential backoff algorithm from Google's HTTP Client Library for Java. Really complicated interface. + +* [rafaeljesus/retry-go](https://github.com/rafaeljesus/retry-go) - looks good, slightly similar as this package, don't have 'simple' `Retry` method + +* [matryer/try](https://github.com/matryer/try) - very popular package, nonintuitive interface (for me) + +# BREAKING CHANGES + +* 5.0.0 + - Complete API redesign: method-based retry operations + - Renamed `Config` type to `Retrier` + - Renamed `NewConfig()` to `New()` + - Changed from package-level functions to methods: `retry.Do(func, config)` → `retry.New(opts...).Do(func)` + - `DelayTypeFunc` signature changed: `func(n uint, err error, config *Config)` → `func(n uint, err error, r *Retrier)` + - Migration: `retry.Do(func, opts...)` → `retry.New(opts...).Do(func)` (simple find & replace) + - This change improves performance, simplifies the API, and provides a cleaner interface + - `Unwrap()` now returns `[]error` instead of `error` to support Go 1.20 multiple error wrapping. + - `errors.Unwrap(err)` will now return `nil` (same as `errors.Join`). Use `errors.Is` or `errors.As` to inspect wrapped errors. + +* 4.0.0 + - infinity retry is possible by set `Attempts(0)` by PR [#49](https://github.com/avast/retry-go/pull/49) + +* 3.0.0 + - `DelayTypeFunc` accepts a new parameter `err` - this breaking change affects only your custom Delay Functions. This change allow [make delay functions based on error](examples/delay_based_on_error_test.go). + +* 1.0.2 -> 2.0.0 + - argument of `retry.Delay` is final delay (no multiplication by `retry.Units` anymore) + - function `retry.Units` are removed + - [more about this breaking change](https://github.com/avast/retry-go/issues/7) + +* 0.3.0 -> 1.0.0 + - `retry.Retry` function are changed to `retry.Do` function + - `retry.RetryCustom` (OnRetry) and `retry.RetryCustomWithOpts` functions are now implement via functions produces Options (aka `retry.OnRetry`) +*/ +package retry + +import ( + "context" + "errors" + "fmt" + "strings" + "time" +) + +// Function signature of retryable function +type RetryableFunc func() error + +// Function signature of retryable function with data +type RetryableFuncWithData[T any] func() (T, error) + +// Default r.timer is a wrapper around time.After +type timerImpl struct{} + +func (t *timerImpl) After(d time.Duration) <-chan time.Time { + return time.After(d) +} + +// Do executes the retryable function using this Retrier's configuration. +func (r *Retrier) Do(retryableFunc RetryableFunc) error { + retryableFuncWithData := func() (any, error) { + return nil, retryableFunc() + } + + _, err := doWithData(r.retrierCore, retryableFuncWithData) + return err +} + +// Do executes the retryable function using this RetrierWithData's configuration. +func (r *RetrierWithData[T]) Do(retryableFunc RetryableFuncWithData[T]) (T, error) { + return doWithData(r.retrierCore, retryableFunc) +} + +func doWithData[T any](r *retrierCore, retryableFunc RetryableFuncWithData[T]) (T, error) { + var emptyT T + var n uint + + if err := context.Cause(r.context); err != nil { + return emptyT, err + } + + // Setting r.attempts to 0 means we'll retry until we succeed + var lastErr error + if r.attempts == 0 { + for { + t, err := retryableFunc() + if err == nil { + return t, nil + } + + if !IsRecoverable(err) { + return emptyT, err + } + + if !r.retryIf(err) { + return emptyT, err + } + + lastErr = err + + r.onRetry(n, err) + n++ + select { + case <-r.timer.After(r.computeDelay(n, err)): + case <-r.context.Done(): + if r.wrapContextErrorWithLastError { + return emptyT, Error{context.Cause(r.context), lastErr} + } + return emptyT, context.Cause(r.context) + } + } + } + + errorLog := Error{} + + attemptsForErrorCopy := make(map[error]uint, len(r.attemptsForError)) + for err, attempts := range r.attemptsForError { + attemptsForErrorCopy[err] = attempts + } + +shouldRetry: + for { + t, err := retryableFunc() + if err == nil { + return t, nil + } + + errorLog = append(errorLog, unpackUnrecoverable(err)) + + if !r.retryIf(err) { + break + } + + r.onRetry(n, err) + + for errToCheck, attemptsForThisError := range attemptsForErrorCopy { + if errors.Is(err, errToCheck) { + attemptsForThisError-- + attemptsForErrorCopy[errToCheck] = attemptsForThisError + if attemptsForThisError <= 0 { + break shouldRetry + } + } + } + + // if this is last attempt - don't wait + if n == r.attempts-1 { + break shouldRetry + } + n++ + select { + case <-r.timer.After(r.computeDelay(n, err)): + case <-r.context.Done(): + if r.lastErrorOnly { + return emptyT, context.Cause(r.context) + } + + return emptyT, append(errorLog, context.Cause(r.context)) + } + } + + if r.lastErrorOnly { + return emptyT, errorLog[len(errorLog)-1] + } + return emptyT, errorLog +} + +// Error type represents list of errors in retry +type Error []error + +// Error method return string representation of Error +// It is an implementation of error interface +func (e Error) Error() string { + logWithNumber := make([]string, len(e)) + for i, l := range e { + if l != nil { + logWithNumber[i] = fmt.Sprintf("#%d: %s", i+1, l.Error()) + } + } + + return fmt.Sprintf("All attempts fail:\n%s", strings.Join(logWithNumber, "\n")) +} + +func (e Error) Is(target error) bool { + for _, v := range e { + if errors.Is(v, target) { + return true + } + } + return false +} + +func (e Error) As(target interface{}) bool { + for _, v := range e { + if errors.As(v, target) { + return true + } + } + return false +} + +// Unwrap returns the list of errors that this Error is wrapping. +// +// This method implements the Unwrap() []error interface introduced in Go 1.20 +// for multi-error unwrapping. This allows errors.Is and errors.As to traverse +// all wrapped errors, not just the last one. +// +// IMPORTANT: errors.Unwrap(err) will return nil because the standard library's +// errors.Unwrap function only calls Unwrap() error, not Unwrap() []error. +// This is the same behavior as errors.Join in Go 1.20. +// +// Example - Use errors.Is to check for specific errors: +// +// err := retry.New(retry.Attempts(3)).Do(func() error { +// return os.ErrNotExist +// }) +// if errors.Is(err, os.ErrNotExist) { +// // Handle not exist error +// } +// +// Example - Use errors.As to extract error details: +// +// var pathErr *fs.PathError +// if errors.As(err, &pathErr) { +// fmt.Println("Failed at path:", pathErr.Path) +// } +// +// Example - Get the last error directly (for migration): +// +// if retryErr, ok := err.(retry.Error); ok { +// lastErr := retryErr.LastError() +// } +// +// See also: LastError() for direct access to the last error. +func (e Error) Unwrap() []error { + return e +} + +// WrappedErrors returns the list of errors that this Error is wrapping. +// It is an implementation of the `errwrap.Wrapper` interface +// in package [errwrap](https://github.com/hashicorp/errwrap) so that +// `retry.Error` can be used with that library. +func (e Error) WrappedErrors() []error { + return e +} + +// LastError returns the last error in the error list. +// +// This is a convenience method for users migrating from retry-go v4.x where +// errors.Unwrap(err) returned the last error. In v5.0.0, errors.Unwrap(err) +// returns nil due to the switch to Unwrap() []error for Go 1.20 compatibility. +// +// Migration example: +// +// // v4.x code: +// lastErr := errors.Unwrap(retryErr) +// +// // v5.0.0 code (option 1 - recommended): +// if errors.Is(retryErr, specificError) { ... } +// +// // v5.0.0 code (option 2 - if you need the last error): +// lastErr := retryErr.(retry.Error).LastError() +// +// Note: Using errors.Is or errors.As is preferred as they check ALL wrapped +// errors, not just the last one. +func (e Error) LastError() error { + if len(e) == 0 { + return nil + } + return e[len(e)-1] +} + +type unrecoverableError struct { + error +} + +func (e unrecoverableError) Error() string { + if e.error == nil { + return "unrecoverable error" + } + return e.error.Error() +} + +func (e unrecoverableError) Unwrap() error { + return e.error +} + +// Unrecoverable wraps an error in `unrecoverableError` struct +func Unrecoverable(err error) error { + return unrecoverableError{err} +} + +// IsRecoverable checks if error is an instance of `unrecoverableError` +func IsRecoverable(err error) bool { + return !errors.Is(err, unrecoverableError{}) +} + +// Adds support for errors.Is usage on unrecoverableError +func (unrecoverableError) Is(err error) bool { + _, isUnrecoverable := err.(unrecoverableError) + return isUnrecoverable +} + +func unpackUnrecoverable(err error) error { + if unrecoverable, isUnrecoverable := err.(unrecoverableError); isUnrecoverable { + return unrecoverable.error + } + + return err +} + +func (r *retrierCore) computeDelay(n uint, err error) time.Duration { + delayTime := r.delayType(n, err, r) + if r.maxDelay > 0 && delayTime > r.maxDelay { + delayTime = r.maxDelay + } + return delayTime +} diff --git a/nodeadm/vendor/modules.txt b/nodeadm/vendor/modules.txt index 0ad976f17..ead3397dc 100644 --- a/nodeadm/vendor/modules.txt +++ b/nodeadm/vendor/modules.txt @@ -9,6 +9,9 @@ dario.cat/mergo # github.com/antlr4-go/antlr/v4 v4.13.0 ## explicit; go 1.20 github.com/antlr4-go/antlr/v4 +# github.com/avast/retry-go/v5 v5.0.0 +## explicit; go 1.20 +github.com/avast/retry-go/v5 # github.com/aws/aws-sdk-go-v2 v1.41.1 ## explicit; go 1.23 github.com/aws/aws-sdk-go-v2/aws