Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion core/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.23.0
v0.24.0
34 changes: 21 additions & 13 deletions core/wait/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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++
Expand Down
58 changes: 47 additions & 11 deletions core/wait/wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()
Expand Down
Loading