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 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..6f1ab2da3 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) } @@ -355,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()