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.26.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.25.0
- Minimal go version is now Go 1.25

Expand Down
2 changes: 1 addition & 1 deletion core/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.25.0
v0.26.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}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've always assumed this is public to allow setting it. With this change I'd be in favour of deprecating this and making it private after the deprecation period.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand this allows to express: "whatever the default is + sth. else" like:

myRetries := append(RetryHttpErrorStatusCodes, http.StatusRequestTimeout)


// 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 @@ -122,7 +130,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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if !utils.Contains(h.retryHttpErrorStatusCodes, oapiErr.StatusCode) {
if !slices.Contains(h.retryHttpErrorStatusCodes, oapiErr.StatusCode) {

I'd prefer the builtin func instead of the utils one, if we are already touching this line

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd also need a fallback here when h.retryHttpErrrorStatusCodes == nil for people that did not use New but a AsyncActionHandler{} literal to create an instance.

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 @@ -23,11 +23,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 @@ -160,7 +161,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...)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, thx!

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 @@ -356,11 +391,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