1package request 2 3import ( 4 "fmt" 5 "time" 6 7 "github.com/aws/aws-sdk-go/aws" 8 "github.com/aws/aws-sdk-go/aws/awserr" 9 "github.com/aws/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. 14const WaiterResourceNotReadyErrorCode = "ResourceNotReady" 15 16// A WaiterOption is a function that will update the Waiter value's fields to 17// configure the waiter. 18type 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. 22func 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. 34type 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. 39func 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. 46func 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. 54func 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. 63func 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. 74type 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. 88func (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. 96type WaiterState int 97 98// String returns the string representation of the waiter state. 99func (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. 113const ( 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. 121type WaiterMatchMode int 122 123// Modes the waiter will use when inspecting API response to identify target 124// resource states. 125const ( 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. 135func (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. 169func (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. 216type 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. 226func (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 291func waiterLogf(logger aws.Logger, msg string, args ...interface{}) { 292 if logger != nil { 293 logger.Log(fmt.Sprintf(msg, args...)) 294 } 295} 296