From 09a7868f4f34b992a0adb48de587f5814c7b9b9f Mon Sep 17 00:00:00 2001 From: "Marcel S. Henselin" Date: Mon, 30 Mar 2026 12:20:28 +0200 Subject: [PATCH 1/3] feat: add missing htt StatusCode and make it settable --- core/wait/wait.go | 34 ++++++++++++++++++------------ core/wait/wait_test.go | 47 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/core/wait/wait.go b/core/wait/wait.go index a0e0a9ca0..4b4d494ff 100644 --- a/core/wait/wait.go +++ b/core/wait/wait.go @@ -10,7 +10,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/utils" ) -var RetryHttpErrorStatusCodes = []int{http.StatusBadGateway, http.StatusGatewayTimeout} +var RetryHttpErrorStatusCodes = []int{http.StatusBadGateway, http.StatusGatewayTimeout, http.StatusServiceUnavailable} // AsyncActionCheck reports whether a specific async action has finished. // - waitFinished == true if the async action is finished, false otherwise. @@ -20,25 +20,33 @@ type AsyncActionCheck[T any] func() (waitFinished bool, response *T, err error) // AsyncActionHandler handles waiting for a specific async action to be finished. type AsyncActionHandler[T any] struct { - checkFn AsyncActionCheck[T] - sleepBeforeWait time.Duration - throttle time.Duration - timeout time.Duration - tempErrRetryLimit int - IntermediateStateReached bool + checkFn AsyncActionCheck[T] + sleepBeforeWait time.Duration + throttle time.Duration + timeout time.Duration + tempErrRetryLimit int + IntermediateStateReached bool + retryHttpErrorStatusCodes []int } // New initializes an AsyncActionHandler func New[T any](f AsyncActionCheck[T]) *AsyncActionHandler[T] { return &AsyncActionHandler[T]{ - checkFn: f, - sleepBeforeWait: 0 * time.Second, - throttle: 5 * time.Second, - timeout: 30 * time.Minute, - tempErrRetryLimit: 5, + checkFn: f, + sleepBeforeWait: 0 * time.Second, + throttle: 5 * time.Second, + timeout: 30 * time.Minute, + tempErrRetryLimit: 5, + retryHttpErrorStatusCodes: RetryHttpErrorStatusCodes, } } +// SetRetryHttpErrorStatusCodes sets the retry codes to use for retry. +func (h *AsyncActionHandler[T]) SetRetryHttpErrorStatusCodes(c []int) *AsyncActionHandler[T] { + h.retryHttpErrorStatusCodes = c + return h +} + // SetThrottle sets the time interval between each check of the async action. func (h *AsyncActionHandler[T]) SetThrottle(d time.Duration) *AsyncActionHandler[T] { h.throttle = d @@ -114,7 +122,7 @@ func (h *AsyncActionHandler[T]) handleError(retryTempErrorCounter int, err error return retryTempErrorCounter, fmt.Errorf("found non-GenericOpenApiError: %w", err) } // Some APIs may return temporary errors and the request should be retried - if !utils.Contains(RetryHttpErrorStatusCodes, oapiErr.StatusCode) { + if !utils.Contains(h.retryHttpErrorStatusCodes, oapiErr.StatusCode) { return retryTempErrorCounter, err } retryTempErrorCounter++ diff --git a/core/wait/wait_test.go b/core/wait/wait_test.go index fd40c0ec6..0bb45ade3 100644 --- a/core/wait/wait_test.go +++ b/core/wait/wait_test.go @@ -22,11 +22,12 @@ func TestNew(t *testing.T) { checkFn := func() (waitFinished bool, res *interface{}, err error) { return true, nil, nil } got := New(checkFn) want := &AsyncActionHandler[interface{}]{ - checkFn: checkFn, - sleepBeforeWait: 0 * time.Second, - throttle: 5 * time.Second, - timeout: 30 * time.Minute, - tempErrRetryLimit: 5, + checkFn: checkFn, + sleepBeforeWait: 0 * time.Second, + throttle: 5 * time.Second, + timeout: 30 * time.Minute, + tempErrRetryLimit: 5, + retryHttpErrorStatusCodes: RetryHttpErrorStatusCodes, } diff := cmp.Diff(got, want, cmpOpts...) @@ -159,7 +160,41 @@ func TestSetTempErrRetryLimit(t *testing.T) { got := New(checkFn) got.SetTempErrRetryLimit(tt.tempErrRetryLimit) - diff := cmp.Diff(got, want, cmpOpts...) + diff := cmp.Diff(want, got, cmpOpts...) + if diff != "" { + t.Errorf("Data does not match: %s", diff) + } + }) + } +} + +func TestSetRetryHttpErrorStatusCodes(t *testing.T) { + checkFn := func() (waitFinished bool, res *interface{}, err error) { return true, nil, nil } + + for _, tt := range []struct { + desc string + setRetryCodes []int + wantRetryCodes []int + }{ + { + "default", + []int{}, + []int{http.StatusBadGateway, http.StatusGatewayTimeout, http.StatusServiceUnavailable}, + }, + { + "base_3", + []int{http.StatusTooManyRequests, http.StatusInternalServerError}, + []int{http.StatusTooManyRequests, http.StatusInternalServerError}, + }, + } { + t.Run(tt.desc, func(t *testing.T) { + want := New(checkFn) + want.retryHttpErrorStatusCodes = tt.wantRetryCodes + got := New(checkFn) + if len(tt.setRetryCodes) != 0 { + got.SetRetryHttpErrorStatusCodes(tt.setRetryCodes) + } + diff := cmp.Diff(want, got, cmpOpts...) if diff != "" { t.Errorf("Data does not match: %s", diff) } From 6ef6083507d83df302ffde2cb2e884a5d3ffa825 Mon Sep 17 00:00:00 2001 From: "Marcel S. Henselin" Date: Mon, 30 Mar 2026 12:28:34 +0200 Subject: [PATCH 2/3] chore: adjust CHANGELOG and VERSION --- core/CHANGELOG.md | 4 ++++ core/VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 06ab74bda..b3168e7bd 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -1,3 +1,7 @@ +## v0.24.0 +- **Feature:** Add `SetRetryHttpErrorStatusCodes` to waiter to be able to configure the errors to retry on +- **New:** add missing StatusServiceUnavailable to list of retry codes + ## v0.23.0 - **New:** Add new `WaiterHelper` struct, which creates an `AsyncActionCheck` function based on the configuration diff --git a/core/VERSION b/core/VERSION index 0c2a959e8..6897c006a 100644 --- a/core/VERSION +++ b/core/VERSION @@ -1 +1 @@ -v0.23.0 +v0.24.0 From 96b5e2a02e1592c903fd83aa188bb48adc288039 Mon Sep 17 00:00:00 2001 From: "Marcel S. Henselin" Date: Mon, 30 Mar 2026 12:39:38 +0200 Subject: [PATCH 3/3] chore: fix missing settig in test --- core/wait/wait_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/wait/wait_test.go b/core/wait/wait_test.go index 0bb45ade3..6f1ab2da3 100644 --- a/core/wait/wait_test.go +++ b/core/wait/wait_test.go @@ -390,11 +390,12 @@ func TestWaitWithContext(t *testing.T) { return false, nil, fmt.Errorf("something bad happened when checking if the async action was finished") } handler := AsyncActionHandler[respType]{ - checkFn: checkFn, - sleepBeforeWait: tt.handlerSleepBeforeWait, - throttle: tt.handlerThrottle, - timeout: tt.handlerTimeout, - tempErrRetryLimit: tt.handlerTempErrRetryLimit, + checkFn: checkFn, + sleepBeforeWait: tt.handlerSleepBeforeWait, + throttle: tt.handlerThrottle, + timeout: tt.handlerTimeout, + tempErrRetryLimit: tt.handlerTempErrRetryLimit, + retryHttpErrorStatusCodes: RetryHttpErrorStatusCodes, } ctx, cancel := context.WithTimeout(context.Background(), tt.contextTimeout) defer cancel()