| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- 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...))
- }
- }
|