]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package request |
2 | ||
3 | import ( | |
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. | |
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 | ||
15c0b25d AP |
82 | RequestOptions []Option |
83 | NewRequest func([]Option) (*Request, error) | |
84 | SleepWithContext func(aws.Context, time.Duration) error | |
bae9f6d2 JC |
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) | |
15c0b25d AP |
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 | } | |
bae9f6d2 JC |
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 | } |