github.com/aavshr/aws-sdk-go@v1.41.3/aws/request/waiter.go (about)

     1  package request
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/aavshr/aws-sdk-go/aws"
     8  	"github.com/aavshr/aws-sdk-go/aws/awserr"
     9  	"github.com/aavshr/aws-sdk-go/aws/awsutil"
    10  )
    11  
    12  // WaiterResourceNotReadyErrorCode is the error code returned by a waiter when
    13  // the waiter's max attempts have been exhausted.
    14  const WaiterResourceNotReadyErrorCode = "ResourceNotReady"
    15  
    16  // A WaiterOption is a function that will update the Waiter value's fields to
    17  // configure the waiter.
    18  type WaiterOption func(*Waiter)
    19  
    20  // WithWaiterMaxAttempts returns the maximum number of times the waiter should
    21  // attempt to check the resource for the target state.
    22  func WithWaiterMaxAttempts(max int) WaiterOption {
    23  	return func(w *Waiter) {
    24  		w.MaxAttempts = max
    25  	}
    26  }
    27  
    28  // WaiterDelay will return a delay the waiter should pause between attempts to
    29  // check the resource state. The passed in attempt is the number of times the
    30  // Waiter has checked the resource state.
    31  //
    32  // Attempt is the number of attempts the Waiter has made checking the resource
    33  // state.
    34  type WaiterDelay func(attempt int) time.Duration
    35  
    36  // ConstantWaiterDelay returns a WaiterDelay that will always return a constant
    37  // delay the waiter should use between attempts. It ignores the number of
    38  // attempts made.
    39  func ConstantWaiterDelay(delay time.Duration) WaiterDelay {
    40  	return func(attempt int) time.Duration {
    41  		return delay
    42  	}
    43  }
    44  
    45  // WithWaiterDelay will set the Waiter to use the WaiterDelay passed in.
    46  func WithWaiterDelay(delayer WaiterDelay) WaiterOption {
    47  	return func(w *Waiter) {
    48  		w.Delay = delayer
    49  	}
    50  }
    51  
    52  // WithWaiterLogger returns a waiter option to set the logger a waiter
    53  // should use to log warnings and errors to.
    54  func WithWaiterLogger(logger aws.Logger) WaiterOption {
    55  	return func(w *Waiter) {
    56  		w.Logger = logger
    57  	}
    58  }
    59  
    60  // WithWaiterRequestOptions returns a waiter option setting the request
    61  // options for each request the waiter makes. Appends to waiter's request
    62  // options already set.
    63  func WithWaiterRequestOptions(opts ...Option) WaiterOption {
    64  	return func(w *Waiter) {
    65  		w.RequestOptions = append(w.RequestOptions, opts...)
    66  	}
    67  }
    68  
    69  // A Waiter provides the functionality to perform a blocking call which will
    70  // wait for a resource state to be satisfied by a service.
    71  //
    72  // This type should not be used directly. The API operations provided in the
    73  // service packages prefixed with "WaitUntil" should be used instead.
    74  type Waiter struct {
    75  	Name      string
    76  	Acceptors []WaiterAcceptor
    77  	Logger    aws.Logger
    78  
    79  	MaxAttempts int
    80  	Delay       WaiterDelay
    81  
    82  	RequestOptions   []Option
    83  	NewRequest       func([]Option) (*Request, error)
    84  	SleepWithContext func(aws.Context, time.Duration) error
    85  }
    86  
    87  // ApplyOptions updates the waiter with the list of waiter options provided.
    88  func (w *Waiter) ApplyOptions(opts ...WaiterOption) {
    89  	for _, fn := range opts {
    90  		fn(w)
    91  	}
    92  }
    93  
    94  // WaiterState are states the waiter uses based on WaiterAcceptor definitions
    95  // to identify if the resource state the waiter is waiting on has occurred.
    96  type WaiterState int
    97  
    98  // String returns the string representation of the waiter state.
    99  func (s WaiterState) String() string {
   100  	switch s {
   101  	case SuccessWaiterState:
   102  		return "success"
   103  	case FailureWaiterState:
   104  		return "failure"
   105  	case RetryWaiterState:
   106  		return "retry"
   107  	default:
   108  		return "unknown waiter state"
   109  	}
   110  }
   111  
   112  // States the waiter acceptors will use to identify target resource states.
   113  const (
   114  	SuccessWaiterState WaiterState = iota // waiter successful
   115  	FailureWaiterState                    // waiter failed
   116  	RetryWaiterState                      // waiter needs to be retried
   117  )
   118  
   119  // WaiterMatchMode is the mode that the waiter will use to match the WaiterAcceptor
   120  // definition's Expected attribute.
   121  type WaiterMatchMode int
   122  
   123  // Modes the waiter will use when inspecting API response to identify target
   124  // resource states.
   125  const (
   126  	PathAllWaiterMatch  WaiterMatchMode = iota // match on all paths
   127  	PathWaiterMatch                            // match on specific path
   128  	PathAnyWaiterMatch                         // match on any path
   129  	PathListWaiterMatch                        // match on list of paths
   130  	StatusWaiterMatch                          // match on status code
   131  	ErrorWaiterMatch                           // match on error
   132  )
   133  
   134  // String returns the string representation of the waiter match mode.
   135  func (m WaiterMatchMode) String() string {
   136  	switch m {
   137  	case PathAllWaiterMatch:
   138  		return "pathAll"
   139  	case PathWaiterMatch:
   140  		return "path"
   141  	case PathAnyWaiterMatch:
   142  		return "pathAny"
   143  	case PathListWaiterMatch:
   144  		return "pathList"
   145  	case StatusWaiterMatch:
   146  		return "status"
   147  	case ErrorWaiterMatch:
   148  		return "error"
   149  	default:
   150  		return "unknown waiter match mode"
   151  	}
   152  }
   153  
   154  // WaitWithContext will make requests for the API operation using NewRequest to
   155  // build API requests. The request's response will be compared against the
   156  // Waiter's Acceptors to determine the successful state of the resource the
   157  // waiter is inspecting.
   158  //
   159  // The passed in context must not be nil. If it is nil a panic will occur. The
   160  // Context will be used to cancel the waiter's pending requests and retry delays.
   161  // Use aws.BackgroundContext if no context is available.
   162  //
   163  // The waiter will continue until the target state defined by the Acceptors,
   164  // or the max attempts expires.
   165  //
   166  // Will return the WaiterResourceNotReadyErrorCode error code if the waiter's
   167  // retryer ShouldRetry returns false. This normally will happen when the max
   168  // wait attempts expires.
   169  func (w Waiter) WaitWithContext(ctx aws.Context) error {
   170  
   171  	for attempt := 1; ; attempt++ {
   172  		req, err := w.NewRequest(w.RequestOptions)
   173  		if err != nil {
   174  			waiterLogf(w.Logger, "unable to create request %v", err)
   175  			return err
   176  		}
   177  		req.Handlers.Build.PushBack(MakeAddToUserAgentFreeFormHandler("Waiter"))
   178  		err = req.Send()
   179  
   180  		// See if any of the acceptors match the request's response, or error
   181  		for _, a := range w.Acceptors {
   182  			if matched, matchErr := a.match(w.Name, w.Logger, req, err); matched {
   183  				return matchErr
   184  			}
   185  		}
   186  
   187  		// The Waiter should only check the resource state MaxAttempts times
   188  		// This is here instead of in the for loop above to prevent delaying
   189  		// unnecessary when the waiter will not retry.
   190  		if attempt == w.MaxAttempts {
   191  			break
   192  		}
   193  
   194  		// Delay to wait before inspecting the resource again
   195  		delay := w.Delay(attempt)
   196  		if sleepFn := req.Config.SleepDelay; sleepFn != nil {
   197  			// Support SleepDelay for backwards compatibility and testing
   198  			sleepFn(delay)
   199  		} else {
   200  			sleepCtxFn := w.SleepWithContext
   201  			if sleepCtxFn == nil {
   202  				sleepCtxFn = aws.SleepWithContext
   203  			}
   204  
   205  			if err := sleepCtxFn(ctx, delay); err != nil {
   206  				return awserr.New(CanceledErrorCode, "waiter context canceled", err)
   207  			}
   208  		}
   209  	}
   210  
   211  	return awserr.New(WaiterResourceNotReadyErrorCode, "exceeded wait attempts", nil)
   212  }
   213  
   214  // A WaiterAcceptor provides the information needed to wait for an API operation
   215  // to complete.
   216  type WaiterAcceptor struct {
   217  	State    WaiterState
   218  	Matcher  WaiterMatchMode
   219  	Argument string
   220  	Expected interface{}
   221  }
   222  
   223  // match returns if the acceptor found a match with the passed in request
   224  // or error. True is returned if the acceptor made a match, error is returned
   225  // if there was an error attempting to perform the match.
   226  func (a *WaiterAcceptor) match(name string, l aws.Logger, req *Request, err error) (bool, error) {
   227  	result := false
   228  	var vals []interface{}
   229  
   230  	switch a.Matcher {
   231  	case PathAllWaiterMatch, PathWaiterMatch:
   232  		// Require all matches to be equal for result to match
   233  		vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument)
   234  		if len(vals) == 0 {
   235  			break
   236  		}
   237  		result = true
   238  		for _, val := range vals {
   239  			if !awsutil.DeepEqual(val, a.Expected) {
   240  				result = false
   241  				break
   242  			}
   243  		}
   244  	case PathAnyWaiterMatch:
   245  		// Only a single match needs to equal for the result to match
   246  		vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument)
   247  		for _, val := range vals {
   248  			if awsutil.DeepEqual(val, a.Expected) {
   249  				result = true
   250  				break
   251  			}
   252  		}
   253  	case PathListWaiterMatch:
   254  		// ignored matcher
   255  	case StatusWaiterMatch:
   256  		s := a.Expected.(int)
   257  		result = s == req.HTTPResponse.StatusCode
   258  	case ErrorWaiterMatch:
   259  		if aerr, ok := err.(awserr.Error); ok {
   260  			result = aerr.Code() == a.Expected.(string)
   261  		}
   262  	default:
   263  		waiterLogf(l, "WARNING: Waiter %s encountered unexpected matcher: %s",
   264  			name, a.Matcher)
   265  	}
   266  
   267  	if !result {
   268  		// If there was no matching result found there is nothing more to do
   269  		// for this response, retry the request.
   270  		return false, nil
   271  	}
   272  
   273  	switch a.State {
   274  	case SuccessWaiterState:
   275  		// waiter completed
   276  		return true, nil
   277  	case FailureWaiterState:
   278  		// Waiter failure state triggered
   279  		return true, awserr.New(WaiterResourceNotReadyErrorCode,
   280  			"failed waiting for successful resource state", err)
   281  	case RetryWaiterState:
   282  		// clear the error and retry the operation
   283  		return false, nil
   284  	default:
   285  		waiterLogf(l, "WARNING: Waiter %s encountered unexpected state: %s",
   286  			name, a.State)
   287  		return false, nil
   288  	}
   289  }
   290  
   291  func waiterLogf(logger aws.Logger, msg string, args ...interface{}) {
   292  	if logger != nil {
   293  		logger.Log(fmt.Sprintf(msg, args...))
   294  	}
   295  }