From 3d52ce56c131b4fd7420c41dde4f9cce3bc99594 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2026 05:51:50 +0000 Subject: [PATCH] fix(deps): update patch digest dependencies --- .github/workflows/build.yml | 2 +- go.mod | 3 +- go.sum | 10 +- .../github.com/avast/retry-go/v4/.gitignore | 21 + .../avast/retry-go/v4/.godocdown.tmpl | 38 ++ vendor/github.com/avast/retry-go/v4/LICENSE | 21 + vendor/github.com/avast/retry-go/v4/Makefile | 59 +++ vendor/github.com/avast/retry-go/v4/README.md | 494 ++++++++++++++++++ vendor/github.com/avast/retry-go/v4/VERSION | 1 + .../github.com/avast/retry-go/v4/current.txt | 26 + .../github.com/avast/retry-go/v4/generic.txt | 46 ++ .../github.com/avast/retry-go/v4/options.go | 311 +++++++++++ vendor/github.com/avast/retry-go/v4/retry.go | 347 ++++++++++++ .../longhorn/go-common-libs/exec/exec.go | 16 +- .../go-common-libs/kubernetes/mount.go | 8 +- .../longhorn/go-common-libs/ns/executor.go | 102 +++- .../longhorn/go-common-libs/types/crypto.go | 1 + .../longhorn/go-common-libs/utils/misc.go | 4 +- vendor/modules.txt | 7 +- 19 files changed, 1501 insertions(+), 16 deletions(-) create mode 100644 vendor/github.com/avast/retry-go/v4/.gitignore create mode 100644 vendor/github.com/avast/retry-go/v4/.godocdown.tmpl create mode 100644 vendor/github.com/avast/retry-go/v4/LICENSE create mode 100644 vendor/github.com/avast/retry-go/v4/Makefile create mode 100644 vendor/github.com/avast/retry-go/v4/README.md create mode 100644 vendor/github.com/avast/retry-go/v4/VERSION create mode 100644 vendor/github.com/avast/retry-go/v4/current.txt create mode 100644 vendor/github.com/avast/retry-go/v4/generic.txt create mode 100644 vendor/github.com/avast/retry-go/v4/options.go create mode 100644 vendor/github.com/avast/retry-go/v4/retry.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4dc9f1b..1d5ec4a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,7 +107,7 @@ jobs: uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Login to Docker Hub - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/go.mod b/go.mod index 099f24c7..85ef447a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/longhorn/cli go 1.26.0 require ( - github.com/longhorn/go-common-libs v0.0.0-20260512083219-bb6c10ce1050 + github.com/longhorn/go-common-libs v0.0.0-20260730002911-add09e6eb92c github.com/longhorn/longhorn-manager v1.12.0 github.com/otiai10/copy v1.14.1 github.com/pkg/errors v0.9.1 @@ -24,6 +24,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + github.com/avast/retry-go/v4 v4.7.0 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 // indirect diff --git a/go.sum b/go.sum index 5f4a1a10..e4544656 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= +github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -135,8 +137,8 @@ github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffkt github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/longhorn/beszel v0.16.2-0.20260114090315-332709c32c7d h1:Fu1U1CyaxN42AItajvTu4iKdyjOJitKIu0NtQc1jye8= github.com/longhorn/beszel v0.16.2-0.20260114090315-332709c32c7d/go.mod h1:369q8Fv+iqznDYF3++boASEe1+fKS8Q4vdSb8isEI1c= -github.com/longhorn/go-common-libs v0.0.0-20260512083219-bb6c10ce1050 h1:2Jaa2XhhkE8dpSWxI+buMzUM6IgQ4wby3SmYd494Bac= -github.com/longhorn/go-common-libs v0.0.0-20260512083219-bb6c10ce1050/go.mod h1:+aXFI8DmHTX4Az9pZF16/XKfrVKWIESJjw+hcxdMslw= +github.com/longhorn/go-common-libs v0.0.0-20260730002911-add09e6eb92c h1:WR2BAcTwBRv2fKxd+z066GWQj2Ioq+m5pPpjK2fvxwk= +github.com/longhorn/go-common-libs v0.0.0-20260730002911-add09e6eb92c/go.mod h1:pjOR2fdQA0dNp3is1xfA+Ni2PjPDP+JuOoSEjdL2cfQ= github.com/longhorn/go-spdk-helper v0.6.2 h1:QcXKAvNIqWpRH+3bq+hgnXh1prjacuQrJ106WfNuIHM= github.com/longhorn/go-spdk-helper v0.6.2/go.mod h1:ZxbMkGsbC2TpzuGLjn9yXXExolt8/F3dl/C8O9/Qa7s= github.com/longhorn/longhorn-manager v1.12.0 h1:XJNQSH1M7P5ot+YoTtpKHTYW3yXNeFLTWbR1My/vs3c= @@ -276,8 +278,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/vendor/github.com/avast/retry-go/v4/.gitignore b/vendor/github.com/avast/retry-go/v4/.gitignore new file mode 100644 index 00000000..c40eb23f --- /dev/null +++ b/vendor/github.com/avast/retry-go/v4/.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/vendor/github.com/avast/retry-go/v4/.godocdown.tmpl b/vendor/github.com/avast/retry-go/v4/.godocdown.tmpl new file mode 100644 index 00000000..32b80df8 --- /dev/null +++ b/vendor/github.com/avast/retry-go/v4/.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=master)](https://codecov.io/github/avast/retry-go?branch=master) +[![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/vendor/github.com/avast/retry-go/v4/LICENSE b/vendor/github.com/avast/retry-go/v4/LICENSE new file mode 100644 index 00000000..f63fca81 --- /dev/null +++ b/vendor/github.com/avast/retry-go/v4/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/vendor/github.com/avast/retry-go/v4/Makefile b/vendor/github.com/avast/retry-go/v4/Makefile new file mode 100644 index 00000000..86544d23 --- /dev/null +++ b/vendor/github.com/avast/retry-go/v4/Makefile @@ -0,0 +1,59 @@ +SOURCE_FILES?=$$(go list ./... | grep -v /vendor/) +TEST_PATTERN?=. +TEST_OPTIONS?= +VERSION?=$$(cat VERSION) +LINTER?=$$(which golangci-lint) +LINTER_VERSION=1.50.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 + @if [ "$(LINTER)" = "" ]; then\ + curl -L https://github.com/golangci/golangci-lint/releases/download/v$(LINTER_VERSION)/$(LINTER_FILE) $(LINTER_UNPACK) ;\ + chmod +x $$GOPATH/bin/golangci-lint;\ + fi + + 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/vendor/github.com/avast/retry-go/v4/README.md b/vendor/github.com/avast/retry-go/v4/README.md new file mode 100644 index 00000000..b7303433 --- /dev/null +++ b/vendor/github.com/avast/retry-go/v4/README.md @@ -0,0 +1,494 @@ +# 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=master)](https://codecov.io/github/avast/retry-go?branch=master) +[![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.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( + 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)) + +[More examples](https://github.com/avast/retry-go/tree/master/examples) + +# SEE ALSO + +* [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 + +* 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 *Config) time.Duration +``` +BackOffDelay is a DelayType which increases delay between consecutive retries + +#### func Do + +```go +func Do(retryableFunc RetryableFunc, opts ...Option) error +``` + +#### func DoWithData + +```go +func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error) +``` + +#### func FixedDelay + +```go +func FixedDelay(_ uint, _ error, config *Config) time.Duration +``` +FixedDelay is a DelayType which keeps delay the same through all iterations + +#### 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 *Config) time.Duration +``` +RandomDelay is a DelayType which picks a random delay up to config.maxJitter + +#### func Unrecoverable + +```go +func Unrecoverable(err error) error +``` +Unrecoverable wraps an error in `unrecoverableError` struct + +#### type Config + +```go +type Config struct { +} +``` + + +#### type DelayTypeFunc + +```go +type DelayTypeFunc func(n uint, err error, config *Config) 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) Unwrap + +```go +func (e Error) Unwrap() error +``` +Unwrap the last error for compatibility with `errors.Unwrap()`. When you need to +unwrap all errors, you should use `WrappedErrors()` instead. + + err := Do( + func() error { + return errors.New("original error") + }, + Attempts(1), + ) + + fmt.Println(errors.Unwrap(err)) # "original error" is printed + +Added in version 4.2.0. + +#### 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(*Config) +``` + +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.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 BackOff + +#### 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.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.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.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.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.Do( + func() error { + ... + }, + retry.Context(ctx), + retry.Attempts(0), + retry.WrapContextErrorWithLastError(true), + ) + +#### 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/vendor/github.com/avast/retry-go/v4/VERSION b/vendor/github.com/avast/retry-go/v4/VERSION new file mode 100644 index 00000000..f6cdf409 --- /dev/null +++ b/vendor/github.com/avast/retry-go/v4/VERSION @@ -0,0 +1 @@ +4.7.0 diff --git a/vendor/github.com/avast/retry-go/v4/current.txt b/vendor/github.com/avast/retry-go/v4/current.txt new file mode 100644 index 00000000..406b14fe --- /dev/null +++ b/vendor/github.com/avast/retry-go/v4/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/vendor/github.com/avast/retry-go/v4/generic.txt b/vendor/github.com/avast/retry-go/v4/generic.txt new file mode 100644 index 00000000..116a0964 --- /dev/null +++ b/vendor/github.com/avast/retry-go/v4/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/vendor/github.com/avast/retry-go/v4/options.go b/vendor/github.com/avast/retry-go/v4/options.go new file mode 100644 index 00000000..3ae28bb1 --- /dev/null +++ b/vendor/github.com/avast/retry-go/v4/options.go @@ -0,0 +1,311 @@ +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) + +// 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 *Config) time.Duration + +// Timer represents the timer used to track time for a retry. +type Timer interface { + After(time.Duration) <-chan time.Time +} + +type Config 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 +} + +// Option represents an option for retry. +type Option func(*Config) + +func emptyOption(c *Config) {} + +// 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(c *Config) { + c.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(c *Config) { + c.attempts = attempts + } +} + +// UntilSucceeded will retry until the retried function succeeds. Equivalent to setting Attempts(0). +func UntilSucceeded() Option { + return func(c *Config) { + c.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(c *Config) { + c.attemptsForError[err] = attempts + } +} + +// Delay set delay between retry +// default is 100ms +func Delay(delay time.Duration) Option { + return func(c *Config) { + c.delay = delay + } +} + +// MaxDelay set maximum delay between retry +// does not apply by default +func MaxDelay(maxDelay time.Duration) Option { + return func(c *Config) { + c.maxDelay = maxDelay + } +} + +// MaxJitter sets the maximum random Jitter between retries for RandomDelay +func MaxJitter(maxJitter time.Duration) Option { + return func(c *Config) { + c.maxJitter = maxJitter + } +} + +// DelayType set type of the delay between retries +// default is BackOff +func DelayType(delayType DelayTypeFunc) Option { + if delayType == nil { + return emptyOption + } + return func(c *Config) { + c.delayType = delayType + } +} + +// BackOffDelay is a DelayType which increases delay between consecutive retries +func BackOffDelay(n uint, _ error, config *Config) time.Duration { + // 1 << 63 would overflow signed int64 (time.Duration), thus 62. + const max uint = 62 + + if config.maxBackOffN == 0 { + if config.delay <= 0 { + config.delay = 1 + } + + config.maxBackOffN = max - uint(math.Floor(math.Log2(float64(config.delay)))) + } + + n-- + + if n > config.maxBackOffN { + n = config.maxBackOffN + } + + return config.delay << n +} + +// FixedDelay is a DelayType which keeps delay the same through all iterations +func FixedDelay(_ uint, _ error, config *Config) time.Duration { + return config.delay +} + +// RandomDelay is a DelayType which picks a random delay up to config.maxJitter +func RandomDelay(_ uint, _ error, config *Config) time.Duration { + return time.Duration(rand.Int63n(int64(config.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 *Config) 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 *Config) 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.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(c *Config) { + c.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.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.Do( +// func() error { +// return retry.Unrecoverable(errors.New("special error")) +// } +// ) +func RetryIf(retryIf RetryIfFunc) Option { + if retryIf == nil { + return emptyOption + } + return func(c *Config) { + c.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.Do( +// func() error { +// ... +// }, +// retry.Context(ctx), +// ) +func Context(ctx context.Context) Option { + return func(c *Config) { + c.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.Do( +// func() error { ... }, +// retry.WithTimer(&MyTimer{}) +// ) +func WithTimer(t Timer) Option { + return func(c *Config) { + c.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.Do( +// func() error { +// ... +// }, +// retry.Context(ctx), +// retry.Attempts(0), +// retry.WrapContextErrorWithLastError(true), +// ) +func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option { + return func(c *Config) { + c.wrapContextErrorWithLastError = wrapContextErrorWithLastError + } +} diff --git a/vendor/github.com/avast/retry-go/v4/retry.go b/vendor/github.com/avast/retry-go/v4/retry.go new file mode 100644 index 00000000..62d6392e --- /dev/null +++ b/vendor/github.com/avast/retry-go/v4/retry.go @@ -0,0 +1,347 @@ +/* +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.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( + 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)) + +[More examples](https://github.com/avast/retry-go/tree/master/examples) + +# SEE ALSO + +* [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 + +* 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 timer is a wrapper around time.After +type timerImpl struct{} + +func (t *timerImpl) After(d time.Duration) <-chan time.Time { + return time.After(d) +} + +func Do(retryableFunc RetryableFunc, opts ...Option) error { + retryableFuncWithData := func() (any, error) { + return nil, retryableFunc() + } + + _, err := DoWithData(retryableFuncWithData, opts...) + return err +} + +func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error) { + var n uint + var emptyT T + + // default + config := newDefaultRetryConfig() + + // apply opts + for _, opt := range opts { + opt(config) + } + + if err := context.Cause(config.context); err != nil { + return emptyT, err + } + + // Setting attempts to 0 means we'll retry until we succeed + var lastErr error + if config.attempts == 0 { + for { + t, err := retryableFunc() + if err == nil { + return t, nil + } + + if !IsRecoverable(err) { + return emptyT, err + } + + if !config.retryIf(err) { + return emptyT, err + } + + lastErr = err + + config.onRetry(n, err) + n++ + select { + case <-config.timer.After(delay(config, n, err)): + case <-config.context.Done(): + if config.wrapContextErrorWithLastError { + return emptyT, Error{context.Cause(config.context), lastErr} + } + return emptyT, context.Cause(config.context) + } + } + } + + errorLog := Error{} + + attemptsForError := make(map[error]uint, len(config.attemptsForError)) + for err, attempts := range config.attemptsForError { + attemptsForError[err] = attempts + } + +shouldRetry: + for { + t, err := retryableFunc() + if err == nil { + return t, nil + } + + errorLog = append(errorLog, unpackUnrecoverable(err)) + + if !config.retryIf(err) { + break + } + + config.onRetry(n, err) + + for errToCheck, attempts := range attemptsForError { + if errors.Is(err, errToCheck) { + attempts-- + attemptsForError[errToCheck] = attempts + if attempts <= 0 { + break shouldRetry + } + } + } + + // if this is last attempt - don't wait + if n == config.attempts-1 { + break shouldRetry + } + n++ + select { + case <-config.timer.After(delay(config, n, err)): + case <-config.context.Done(): + if config.lastErrorOnly { + return emptyT, context.Cause(config.context) + } + + return emptyT, append(errorLog, context.Cause(config.context)) + } + } + + if config.lastErrorOnly { + return emptyT, errorLog.Unwrap() + } + return emptyT, errorLog +} + +func newDefaultRetryConfig() *Config { + return &Config{ + 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{}, + } +} + +// 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 the last error for compatibility with `errors.Unwrap()`. +When you need to unwrap all errors, you should use `WrappedErrors()` instead. + + err := Do( + func() error { + return errors.New("original error") + }, + Attempts(1), + ) + + fmt.Println(errors.Unwrap(err)) # "original error" is printed + +Added in version 4.2.0. +*/ +func (e Error) Unwrap() error { + return e[len(e)-1] +} + +// 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 +} + +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 delay(config *Config, n uint, err error) time.Duration { + delayTime := config.delayType(n, err, config) + if config.maxDelay > 0 && delayTime > config.maxDelay { + delayTime = config.maxDelay + } + + return delayTime +} diff --git a/vendor/github.com/longhorn/go-common-libs/exec/exec.go b/vendor/github.com/longhorn/go-common-libs/exec/exec.go index 77024581..faef7def 100644 --- a/vendor/github.com/longhorn/go-common-libs/exec/exec.go +++ b/vendor/github.com/longhorn/go-common-libs/exec/exec.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "strings" + "syscall" "time" "github.com/cockroachdb/errors" @@ -74,9 +75,18 @@ func (e *Executor) executeCmd(cmd *exec.Cmd, timeout time.Duration) (string, err cmd.Stdout = &output cmd.Stderr = &stderr + // Place the command in its own process group so that a timeout can kill + // the command together with any processes it spawned. + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + if err := cmd.Start(); err != nil { + return "", errors.Wrapf(err, "failed to execute: %v %v, output %s, stderr %s", + cmd.Path, cmd.Args, output.String(), stderr.String()) + } + errChan := make(chan error, 1) go func() { - errChan <- cmd.Run() + errChan <- cmd.Wait() }() select { @@ -86,6 +96,10 @@ func (e *Executor) executeCmd(cmd *exec.Cmd, timeout time.Duration) (string, err cmd.Path, cmd.Args, output.String(), stderr.String()) } case <-ctx.Done(): + // Kill the process group; otherwise the command would keep running + // (and consuming resources) after the timeout error is returned. + // The Wait in the goroutine above reaps the process once it exits. + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) return "", errors.Errorf("timeout executing: %v %v", cmd.Path, cmd.Args) } diff --git a/vendor/github.com/longhorn/go-common-libs/kubernetes/mount.go b/vendor/github.com/longhorn/go-common-libs/kubernetes/mount.go index f0035c7a..6d348e6e 100644 --- a/vendor/github.com/longhorn/go-common-libs/kubernetes/mount.go +++ b/vendor/github.com/longhorn/go-common-libs/kubernetes/mount.go @@ -6,7 +6,13 @@ import ( func IsMountPointReadOnly(mp mount.MountPoint) bool { for _, opt := range mp.Opts { - if opt == "ro" { + // "ro" is the standard read-only mount option. "emergency_ro" is the + // ext4 emergency read-only state: since kernel v6.12 (commit + // d3476f3dad4a "ext4: don't set SB_RDONLY after filesystem errors"), + // errors=remount-ro no longer sets SB_RDONLY, so the mount keeps + // showing "rw" while writes fail with EROFS and the state is only + // visible as the "emergency_ro" option. + if opt == "ro" || opt == "emergency_ro" { return true } } diff --git a/vendor/github.com/longhorn/go-common-libs/ns/executor.go b/vendor/github.com/longhorn/go-common-libs/ns/executor.go index e5cf3f18..a01702d9 100644 --- a/vendor/github.com/longhorn/go-common-libs/ns/executor.go +++ b/vendor/github.com/longhorn/go-common-libs/ns/executor.go @@ -2,20 +2,34 @@ package ns import ( "path/filepath" + "regexp" + "sync" "time" + "github.com/avast/retry-go/v4" "github.com/cockroachdb/errors" + log "github.com/sirupsen/logrus" + "github.com/longhorn/go-common-libs/exec" "github.com/longhorn/go-common-libs/proc" "github.com/longhorn/go-common-libs/types" ) -// Executor is a struct resonpsible for executing commands in a specific +const ( + maxNsDirRefreshRetries = 10 + nsDirRefreshRetryInterval = 1 * time.Second +) + +// Executor is a struct responsible for executing commands in a specific // namespace using nsenter. type Executor struct { + mu sync.RWMutex + namespaces []types.Namespace // The namespaces to enter. nsDirectory string // The directory of the namespace. + processName string // The name of the associated process. + processDir string // The process directory. executor exec.ExecuteInterface // An interface for executing commands. This allows mocking for unit tests. } @@ -35,6 +49,8 @@ func NewNamespaceExecutor(processName, procDirectory string, namespaces []types. NamespaceExecutor := &Executor{ namespaces: namespaces, nsDirectory: nsDir, + processName: processName, + processDir: procDirectory, executor: exec.NewExecutor(), } @@ -45,6 +61,17 @@ func NewNamespaceExecutor(processName, procDirectory string, namespaces []types. return NamespaceExecutor, nil } +// refresh updates nsDirectory in case the corresponding process was restarted and the cached nsDirectory is no longer valid. +// It returns an error if the process is not found or the namespace directory cannot be determined. +func (nsexec *Executor) refresh() error { + nsDir, err := proc.GetProcessNamespaceDirectory(nsexec.processName, nsexec.processDir) + if err != nil { + return err + } + nsexec.nsDirectory = nsDir + return nil +} + // prepareCommandArgs prepares the nsenter command arguments, and the environment variables are not ignored. func (nsexec *Executor) prepareCommandArgs(binary string, args, envs []string) []string { cmdArgs := []string{} @@ -71,17 +98,84 @@ func (nsexec *Executor) prepareCommandArgs(binary string, args, envs []string) [ // Execute executes the command in the namespace. If NsDirectory is empty, // it will execute the command in the current namespace. func (nsexec *Executor) Execute(envs []string, binary string, args []string, timeout time.Duration) (string, error) { - return nsexec.executor.Execute(nil, types.NsBinary, nsexec.prepareCommandArgs(binary, args, envs), timeout) + return nsexec.executeWithRetry(func() (string, error) { + return nsexec.executor.Execute(nil, types.NsBinary, nsexec.prepareCommandArgs(binary, args, envs), timeout) + }) } // ExecuteWithStdin executes the command in the namespace with stdin. // If NsDirectory is empty, it will execute the command in the current namespace. func (nsexec *Executor) ExecuteWithStdin(envs []string, binary string, args []string, stdinString string, timeout time.Duration) (string, error) { - return nsexec.executor.ExecuteWithStdin(types.NsBinary, nsexec.prepareCommandArgs(binary, args, envs), stdinString, timeout) + return nsexec.executeWithRetry(func() (string, error) { + return nsexec.executor.ExecuteWithStdin(types.NsBinary, nsexec.prepareCommandArgs(binary, args, envs), stdinString, timeout) + }) } // ExecuteWithStdinPipe executes the command in the namespace with stdin pipe. // If NsDirectory is empty, it will execute the command in the current namespace. func (nsexec *Executor) ExecuteWithStdinPipe(envs []string, binary string, args []string, stdinString string, timeout time.Duration) (string, error) { - return nsexec.executor.ExecuteWithStdinPipe(types.NsBinary, nsexec.prepareCommandArgs(binary, args, envs), stdinString, timeout) + return nsexec.executeWithRetry(func() (string, error) { + return nsexec.executor.ExecuteWithStdinPipe(types.NsBinary, nsexec.prepareCommandArgs(binary, args, envs), stdinString, timeout) + }) +} + +// staleNsDirPattern matches nsenter errors when the namespace directory no +// longer exists, e.g.: +// +// nsenter: cannot open /proc/12345/ns/mnt: No such file or directory +var staleNsDirPattern = regexp.MustCompile(`nsenter: cannot open .+/ns/.+: No such file or directory`) + +// isNsDirStaleError checks if the error indicates that the cached namespace +// directory is stale (the process was restarted and its PID changed). +func (nsexec *Executor) isNsDirStaleError(err error) bool { + if err == nil { + return false + } + return staleNsDirPattern.MatchString(err.Error()) +} + +// executeWithRetry wraps an execution function with retry logic that detects +// stale namespace directories and refreshes them. When the cached process PID +// becomes stale (e.g., iscsid restarted), nsenter fails with "No such file or +// directory". This method retries after refreshing the namespace directory. +func (nsexec *Executor) executeWithRetry(execFn func() (string, error)) (output string, err error) { + retryErr := retry.Do( + func() error { + nsexec.mu.RLock() + output, err = execFn() + nsexec.mu.RUnlock() + + return err + }, + retry.Attempts(maxNsDirRefreshRetries), + retry.Delay(nsDirRefreshRetryInterval), + retry.DelayType(retry.FixedDelay), + retry.LastErrorOnly(true), + retry.RetryIf(func(err error) bool { + return nsexec.isNsDirStaleError(err) + }), + retry.OnRetry(func(n uint, err error) { + nsexec.mu.Lock() + defer nsexec.mu.Unlock() + + log.WithError(err).Warnf( + "Detected stale namespace directory, refreshing: process=%s procDir=%s cachedNsDir=%s", + nsexec.processName, nsexec.processDir, nsexec.nsDirectory, + ) + + if refreshErr := nsexec.refresh(); refreshErr != nil { + log.WithError(refreshErr).Warnf( + "Failed to refresh namespace directory: process=%s procDir=%s", + nsexec.processName, nsexec.processDir, + ) + } + }), + ) + if retryErr != nil { + if err != nil { + return output, errors.Wrapf(err, "failed after %d attempts to refresh stale namespace directory", maxNsDirRefreshRetries) + } + return output, retryErr + } + return output, nil } diff --git a/vendor/github.com/longhorn/go-common-libs/types/crypto.go b/vendor/github.com/longhorn/go-common-libs/types/crypto.go index b26149ec..9c2b33e2 100644 --- a/vendor/github.com/longhorn/go-common-libs/types/crypto.go +++ b/vendor/github.com/longhorn/go-common-libs/types/crypto.go @@ -15,6 +15,7 @@ const ( CryptoPBKDFMemory = "CRYPTO_PBKDF_MEMORY" CliAPIVersionForSupportingExtendLuks2HeaderSize = 12 + CliAPIVersionExtraLUKS2HeaderReservation = CliAPIVersionForSupportingExtendLuks2HeaderSize Luks2EncryptionHeaderSize = 16 * 1024 * 1024 ) diff --git a/vendor/github.com/longhorn/go-common-libs/utils/misc.go b/vendor/github.com/longhorn/go-common-libs/utils/misc.go index 8cdf8033..a983315b 100644 --- a/vendor/github.com/longhorn/go-common-libs/utils/misc.go +++ b/vendor/github.com/longhorn/go-common-libs/utils/misc.go @@ -1,6 +1,7 @@ package utils import ( + "cmp" "crypto/rand" "fmt" "math/big" @@ -13,7 +14,6 @@ import ( "github.com/cockroachdb/errors" "github.com/google/uuid" - "golang.org/x/exp/constraints" "k8s.io/apimachinery/pkg/util/version" @@ -125,7 +125,7 @@ func ConvertTypeToString[T any](value T) string { } // SortKeys sorts the keys of a map in ascending order. -func SortKeys[K constraints.Ordered, V any](mapObj map[K]V) ([]K, error) { +func SortKeys[K cmp.Ordered, V any](mapObj map[K]V) ([]K, error) { if mapObj == nil { return nil, fmt.Errorf("input object cannot be nil") } diff --git a/vendor/modules.txt b/vendor/modules.txt index 0afac988..4ca8b101 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -8,6 +8,9 @@ github.com/MakeNowJust/heredoc # github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be ## explicit; go 1.13 github.com/anmitsu/go-shlex +# github.com/avast/retry-go/v4 v4.7.0 +## explicit; go 1.20 +github.com/avast/retry-go/v4 # github.com/blang/semver v3.5.1+incompatible ## explicit github.com/blang/semver @@ -229,8 +232,8 @@ github.com/kr/text # github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de ## explicit github.com/liggitt/tabwriter -# github.com/longhorn/go-common-libs v0.0.0-20260512083219-bb6c10ce1050 -## explicit; go 1.25.0 +# github.com/longhorn/go-common-libs v0.0.0-20260730002911-add09e6eb92c +## explicit; go 1.25.10 github.com/longhorn/go-common-libs/exec github.com/longhorn/go-common-libs/io github.com/longhorn/go-common-libs/kubernetes