| package request |
| |
| import ( |
| "fmt" |
| "time" |
| |
| "github.com/aws/aws-sdk-go/aws" |
| "github.com/aws/aws-sdk-go/aws/awserr" |
| "github.com/aws/aws-sdk-go/aws/awsutil" |
| ) |
| |
| // WaiterResourceNotReadyErrorCode is the error code returned by a waiter when |
| // the waiter's max attempts have been exhausted. |
| const WaiterResourceNotReadyErrorCode = "ResourceNotReady" |
| |
| // A WaiterOption is a function that will update the Waiter value's fields to |
| // configure the waiter. |
| type WaiterOption func(*Waiter) |
| |
| // WithWaiterMaxAttempts returns the maximum number of times the waiter should |
| // attempt to check the resource for the target state. |
| func WithWaiterMaxAttempts(max int) WaiterOption { |
| return func(w *Waiter) { |
| w.MaxAttempts = max |
| } |
| } |
| |
| // WaiterDelay will return a delay the waiter should pause between attempts to |
| // check the resource state. The passed in attempt is the number of times the |
| // Waiter has checked the resource state. |
| // |
| // Attempt is the number of attempts the Waiter has made checking the resource |
| // state. |
| type WaiterDelay func(attempt int) time.Duration |
| |
| // ConstantWaiterDelay returns a WaiterDelay that will always return a constant |
| // delay the waiter should use between attempts. It ignores the number of |
| // attempts made. |
| func ConstantWaiterDelay(delay time.Duration) WaiterDelay { |
| return func(attempt int) time.Duration { |
| return delay |
| } |
| } |
| |
| // WithWaiterDelay will set the Waiter to use the WaiterDelay passed in. |
| func WithWaiterDelay(delayer WaiterDelay) WaiterOption { |
| return func(w *Waiter) { |
| w.Delay = delayer |
| } |
| } |
| |
| // WithWaiterLogger returns a waiter option to set the logger a waiter |
| // should use to log warnings and errors to. |
| func WithWaiterLogger(logger aws.Logger) WaiterOption { |
| return func(w *Waiter) { |
| w.Logger = logger |
| } |
| } |
| |
| // WithWaiterRequestOptions returns a waiter option setting the request |
| // options for each request the waiter makes. Appends to waiter's request |
| // options already set. |
| func WithWaiterRequestOptions(opts ...Option) WaiterOption { |
| return func(w *Waiter) { |
| w.RequestOptions = append(w.RequestOptions, opts...) |
| } |
| } |
| |
| // A Waiter provides the functionality to perform a blocking call which will |
| // wait for a resource state to be satisfied by a service. |
| // |
| // This type should not be used directly. The API operations provided in the |
| // service packages prefixed with "WaitUntil" should be used instead. |
| type Waiter struct { |
| Name string |
| Acceptors []WaiterAcceptor |
| Logger aws.Logger |
| |
| MaxAttempts int |
| Delay WaiterDelay |
| |
| RequestOptions []Option |
| NewRequest func([]Option) (*Request, error) |
| SleepWithContext func(aws.Context, time.Duration) error |
| } |
| |
| // ApplyOptions updates the waiter with the list of waiter options provided. |
| func (w *Waiter) ApplyOptions(opts ...WaiterOption) { |
| for _, fn := range opts { |
| fn(w) |
| } |
| } |
| |
| // WaiterState are states the waiter uses based on WaiterAcceptor definitions |
| // to identify if the resource state the waiter is waiting on has occurred. |
| type WaiterState int |
| |
| // String returns the string representation of the waiter state. |
| func (s WaiterState) String() string { |
| switch s { |
| case SuccessWaiterState: |
| return "success" |
| case FailureWaiterState: |
| return "failure" |
| case RetryWaiterState: |
| return "retry" |
| default: |
| return "unknown waiter state" |
| } |
| } |
| |
| // States the waiter acceptors will use to identify target resource states. |
| const ( |
| SuccessWaiterState WaiterState = iota // waiter successful |
| FailureWaiterState // waiter failed |
| RetryWaiterState // waiter needs to be retried |
| ) |
| |
| // WaiterMatchMode is the mode that the waiter will use to match the WaiterAcceptor |
| // definition's Expected attribute. |
| type WaiterMatchMode int |
| |
| // Modes the waiter will use when inspecting API response to identify target |
| // resource states. |
| const ( |
| PathAllWaiterMatch WaiterMatchMode = iota // match on all paths |
| PathWaiterMatch // match on specific path |
| PathAnyWaiterMatch // match on any path |
| PathListWaiterMatch // match on list of paths |
| StatusWaiterMatch // match on status code |
| ErrorWaiterMatch // match on error |
| ) |
| |
| // String returns the string representation of the waiter match mode. |
| func (m WaiterMatchMode) String() string { |
| switch m { |
| case PathAllWaiterMatch: |
| return "pathAll" |
| case PathWaiterMatch: |
| return "path" |
| case PathAnyWaiterMatch: |
| return "pathAny" |
| case PathListWaiterMatch: |
| return "pathList" |
| case StatusWaiterMatch: |
| return "status" |
| case ErrorWaiterMatch: |
| return "error" |
| default: |
| return "unknown waiter match mode" |
| } |
| } |
| |
| // WaitWithContext will make requests for the API operation using NewRequest to |
| // build API requests. The request's response will be compared against the |
| // Waiter's Acceptors to determine the successful state of the resource the |
| // waiter is inspecting. |
| // |
| // The passed in context must not be nil. If it is nil a panic will occur. The |
| // Context will be used to cancel the waiter's pending requests and retry delays. |
| // Use aws.BackgroundContext if no context is available. |
| // |
| // The waiter will continue until the target state defined by the Acceptors, |
| // or the max attempts expires. |
| // |
| // Will return the WaiterResourceNotReadyErrorCode error code if the waiter's |
| // retryer ShouldRetry returns false. This normally will happen when the max |
| // wait attempts expires. |
| func (w Waiter) WaitWithContext(ctx aws.Context) error { |
| |
| for attempt := 1; ; attempt++ { |
| req, err := w.NewRequest(w.RequestOptions) |
| if err != nil { |
| waiterLogf(w.Logger, "unable to create request %v", err) |
| return err |
| } |
| req.Handlers.Build.PushBack(MakeAddToUserAgentFreeFormHandler("Waiter")) |
| err = req.Send() |
| |
| // See if any of the acceptors match the request's response, or error |
| for _, a := range w.Acceptors { |
| if matched, matchErr := a.match(w.Name, w.Logger, req, err); matched { |
| return matchErr |
| } |
| } |
| |
| // The Waiter should only check the resource state MaxAttempts times |
| // This is here instead of in the for loop above to prevent delaying |
| // unnecessary when the waiter will not retry. |
| if attempt == w.MaxAttempts { |
| break |
| } |
| |
| // Delay to wait before inspecting the resource again |
| delay := w.Delay(attempt) |
| if sleepFn := req.Config.SleepDelay; sleepFn != nil { |
| // Support SleepDelay for backwards compatibility and testing |
| sleepFn(delay) |
| } else { |
| sleepCtxFn := w.SleepWithContext |
| if sleepCtxFn == nil { |
| sleepCtxFn = aws.SleepWithContext |
| } |
| |
| if err := sleepCtxFn(ctx, delay); err != nil { |
| return awserr.New(CanceledErrorCode, "waiter context canceled", err) |
| } |
| } |
| } |
| |
| return awserr.New(WaiterResourceNotReadyErrorCode, "exceeded wait attempts", nil) |
| } |
| |
| // A WaiterAcceptor provides the information needed to wait for an API operation |
| // to complete. |
| type WaiterAcceptor struct { |
| State WaiterState |
| Matcher WaiterMatchMode |
| Argument string |
| Expected interface{} |
| } |
| |
| // match returns if the acceptor found a match with the passed in request |
| // or error. True is returned if the acceptor made a match, error is returned |
| // if there was an error attempting to perform the match. |
| func (a *WaiterAcceptor) match(name string, l aws.Logger, req *Request, err error) (bool, error) { |
| result := false |
| var vals []interface{} |
| |
| switch a.Matcher { |
| case PathAllWaiterMatch, PathWaiterMatch: |
| // Require all matches to be equal for result to match |
| vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument) |
| if len(vals) == 0 { |
| break |
| } |
| result = true |
| for _, val := range vals { |
| if !awsutil.DeepEqual(val, a.Expected) { |
| result = false |
| break |
| } |
| } |
| case PathAnyWaiterMatch: |
| // Only a single match needs to equal for the result to match |
| vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument) |
| for _, val := range vals { |
| if awsutil.DeepEqual(val, a.Expected) { |
| result = true |
| break |
| } |
| } |
| case PathListWaiterMatch: |
| // ignored matcher |
| case StatusWaiterMatch: |
| s := a.Expected.(int) |
| result = s == req.HTTPResponse.StatusCode |
| case ErrorWaiterMatch: |
| if aerr, ok := err.(awserr.Error); ok { |
| result = aerr.Code() == a.Expected.(string) |
| } |
| default: |
| waiterLogf(l, "WARNING: Waiter %s encountered unexpected matcher: %s", |
| name, a.Matcher) |
| } |
| |
| if !result { |
| // If there was no matching result found there is nothing more to do |
| // for this response, retry the request. |
| return false, nil |
| } |
| |
| switch a.State { |
| case SuccessWaiterState: |
| // waiter completed |
| return true, nil |
| case FailureWaiterState: |
| // Waiter failure state triggered |
| return true, awserr.New(WaiterResourceNotReadyErrorCode, |
| "failed waiting for successful resource state", err) |
| case RetryWaiterState: |
| // clear the error and retry the operation |
| return false, nil |
| default: |
| waiterLogf(l, "WARNING: Waiter %s encountered unexpected state: %s", |
| name, a.State) |
| return false, nil |
| } |
| } |
| |
| func waiterLogf(logger aws.Logger, msg string, args ...interface{}) { |
| if logger != nil { |
| logger.Log(fmt.Sprintf(msg, args...)) |
| } |
| } |