Skip to content

Commit

Permalink
Revamp wait package (#131)
Browse files Browse the repository at this point in the history
* Waiter changes in core:
- Change signature of wait function
- Change type names

Waiter changes in service modules and examples:
- Fix comments

* Cleanup, uniformize Set signatures

* Rename field

* Revamp TestNew

* Move cmp opts to global var

* Rewrite tests for setters

* Rename field

* Fix typo

* Revamp TestWaitWithContext

* Simplify handleError

* Simplify TestHandleError

* Lint fix

* Add test case

* Rename tests

* Increase test time

* Small change

* Comment reword

* Comment reword

* Rename var

* Remove leftover code

* Add comment

* Remove rule from AsyncActionCheck

* Fix typo

* Change rule regarding AsyncActionCheck

* Uniformize and simplify wait handler implementation

* Update test

* Fix tests

* Fix tests

---------

Co-authored-by: Henrique Santos <[email protected]>
  • Loading branch information
hcsa73 and Henrique Santos authored Oct 27, 2023
1 parent 6440020 commit c8ddfca
Show file tree
Hide file tree
Showing 31 changed files with 1,243 additions and 1,150 deletions.
91 changes: 48 additions & 43 deletions core/wait/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,72 +12,77 @@ import (

var RetryHttpErrorStatusCodes = []int{http.StatusBadGateway, http.StatusGatewayTimeout}

type WaitFn func() (res interface{}, done bool, err error)

type Handler struct {
fn WaitFn
// AsyncActionCheck reports whether a specific async action has finished.
// - waitFinished == true if the async action is finished, false otherwise.
// - response contains data regarding the current state of the resource targeted by the async action, if applicable. If not applicable, T should be struct{}.
// - err != nil if there was an error checking if the async action finished, or if it finished unsuccessfully.
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
}

// New creates a new Wait instance
func New(f WaitFn) *Handler {
return &Handler{
fn: f,
// 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,
}
}

// SetThrottle sets the duration between func triggering
func (w *Handler) SetThrottle(d time.Duration) error {
if d == 0 {
return fmt.Errorf("throttle can't be 0")
}
w.throttle = d
return nil
// 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
return h
}

// SetTimeout sets the duration for wait timeout
func (w *Handler) SetTimeout(d time.Duration) *Handler {
w.timeout = d
return w
// SetTimeout sets the duration for wait timeout.
func (h *AsyncActionHandler[T]) SetTimeout(d time.Duration) *AsyncActionHandler[T] {
h.timeout = d
return h
}

// SetSleepBeforeWait sets the duration for sleep before wait
func (w *Handler) SetSleepBeforeWait(d time.Duration) *Handler {
w.sleepBeforeWait = d
return w
// SetSleepBeforeWait sets the duration for sleep before wait.
func (h *AsyncActionHandler[T]) SetSleepBeforeWait(d time.Duration) *AsyncActionHandler[T] {
h.sleepBeforeWait = d
return h
}

// SetRetryLimitTempErr sets the retry limit if a temporary error is found. The list of temporary errors is defined in the RetryHttpErrorStatusCodes variable
func (w *Handler) SetRetryLimitTempErr(l int) *Handler {
w.tempErrRetryLimit = l
return w
// SetTempErrRetryLimit sets the retry limit if a temporary error is found.
// The list of temporary errors is defined in the RetryHttpErrorStatusCodes variable.
func (h *AsyncActionHandler[T]) SetTempErrRetryLimit(l int) *AsyncActionHandler[T] {
h.tempErrRetryLimit = l
return h
}

// WaitWithContext starts the wait until there's an error or wait is done
func (w *Handler) WaitWithContext(ctx context.Context) (res interface{}, err error) {
var done bool
func (h *AsyncActionHandler[T]) WaitWithContext(ctx context.Context) (res *T, err error) {
if h.throttle == 0 {
return nil, fmt.Errorf("throttle can't be 0")
}

ctx, cancel := context.WithTimeout(ctx, w.timeout)
ctx, cancel := context.WithTimeout(ctx, h.timeout)
defer cancel()

// Wait some seconds for the API to process the request
time.Sleep(w.sleepBeforeWait)
time.Sleep(h.sleepBeforeWait)

ticker := time.NewTicker(w.throttle)
ticker := time.NewTicker(h.throttle)
defer ticker.Stop()

var retryTempErrorCounter = 0
for {
res, done, err = w.fn()
done, res, err := h.checkFn()
if err != nil {
retryTempErrorCounter, err = w.handleError(retryTempErrorCounter, err)
retryTempErrorCounter, err = h.handleError(retryTempErrorCounter, err)
if err != nil {
return res, err
}
Expand All @@ -95,18 +100,18 @@ func (w *Handler) WaitWithContext(ctx context.Context) (res interface{}, err err
}
}

func (w *Handler) handleError(retryTempErrorCounter int, err error) (int, error) {
func (h *AsyncActionHandler[T]) handleError(retryTempErrorCounter int, err error) (int, error) {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if !ok {
return retryTempErrorCounter, fmt.Errorf("could not convert error to GenericOpenApiError, %w", err)
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) {
retryTempErrorCounter++
if retryTempErrorCounter == w.tempErrRetryLimit {
return retryTempErrorCounter, fmt.Errorf("temporary error was found and the retry limit was reached: %w", err)
}
return retryTempErrorCounter, nil
if !utils.Contains(RetryHttpErrorStatusCodes, oapiErr.StatusCode) {
return retryTempErrorCounter, err
}
retryTempErrorCounter++
if retryTempErrorCounter == h.tempErrRetryLimit {
return retryTempErrorCounter, fmt.Errorf("temporary error was found and the retry limit was reached: %w", err)
}
return retryTempErrorCounter, fmt.Errorf("executing wait function: %w", err)
return retryTempErrorCounter, nil
}
Loading

0 comments on commit c8ddfca

Please sign in to comment.