]>
Commit | Line | Data |
---|---|---|
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 | ||
82 | RequestOptions []Option | |
83 | NewRequest func([]Option) (*Request, error) | |
84 | } | |
85 | ||
86 | // ApplyOptions updates the waiter with the list of waiter options provided. | |
87 | func (w *Waiter) ApplyOptions(opts ...WaiterOption) { | |
88 | for _, fn := range opts { | |
89 | fn(w) | |
90 | } | |
91 | } | |
92 | ||
93 | // WaiterState are states the waiter uses based on WaiterAcceptor definitions | |
94 | // to identify if the resource state the waiter is waiting on has occurred. | |
95 | type WaiterState int | |
96 | ||
97 | // String returns the string representation of the waiter state. | |
98 | func (s WaiterState) String() string { | |
99 | switch s { | |
100 | case SuccessWaiterState: | |
101 | return "success" | |
102 | case FailureWaiterState: | |
103 | return "failure" | |
104 | case RetryWaiterState: | |
105 | return "retry" | |
106 | default: | |
107 | return "unknown waiter state" | |
108 | } | |
109 | } | |
110 | ||
111 | // States the waiter acceptors will use to identify target resource states. | |
112 | const ( | |
113 | SuccessWaiterState WaiterState = iota // waiter successful | |
114 | FailureWaiterState // waiter failed | |
115 | RetryWaiterState // waiter needs to be retried | |
116 | ) | |
117 | ||
118 | // WaiterMatchMode is the mode that the waiter will use to match the WaiterAcceptor | |
119 | // definition's Expected attribute. | |
120 | type WaiterMatchMode int | |
121 | ||
122 | // Modes the waiter will use when inspecting API response to identify target | |
123 | // resource states. | |
124 | const ( | |
125 | PathAllWaiterMatch WaiterMatchMode = iota // match on all paths | |
126 | PathWaiterMatch // match on specific path | |
127 | PathAnyWaiterMatch // match on any path | |
128 | PathListWaiterMatch // match on list of paths | |
129 | StatusWaiterMatch // match on status code | |
130 | ErrorWaiterMatch // match on error | |
131 | ) | |
132 | ||
133 | // String returns the string representation of the waiter match mode. | |
134 | func (m WaiterMatchMode) String() string { | |
135 | switch m { | |
136 | case PathAllWaiterMatch: | |
137 | return "pathAll" | |
138 | case PathWaiterMatch: | |
139 | return "path" | |
140 | case PathAnyWaiterMatch: | |
141 | return "pathAny" | |
142 | case PathListWaiterMatch: | |
143 | return "pathList" | |
144 | case StatusWaiterMatch: | |
145 | return "status" | |
146 | case ErrorWaiterMatch: | |
147 | return "error" | |
148 | default: | |
149 | return "unknown waiter match mode" | |
150 | } | |
151 | } | |
152 | ||
153 | // WaitWithContext will make requests for the API operation using NewRequest to | |
154 | // build API requests. The request's response will be compared against the | |
155 | // Waiter's Acceptors to determine the successful state of the resource the | |
156 | // waiter is inspecting. | |
157 | // | |
158 | // The passed in context must not be nil. If it is nil a panic will occur. The | |
159 | // Context will be used to cancel the waiter's pending requests and retry delays. | |
160 | // Use aws.BackgroundContext if no context is available. | |
161 | // | |
162 | // The waiter will continue until the target state defined by the Acceptors, | |
163 | // or the max attempts expires. | |
164 | // | |
165 | // Will return the WaiterResourceNotReadyErrorCode error code if the waiter's | |
166 | // retryer ShouldRetry returns false. This normally will happen when the max | |
167 | // wait attempts expires. | |
168 | func (w Waiter) WaitWithContext(ctx aws.Context) error { | |
169 | ||
170 | for attempt := 1; ; attempt++ { | |
171 | req, err := w.NewRequest(w.RequestOptions) | |
172 | if err != nil { | |
173 | waiterLogf(w.Logger, "unable to create request %v", err) | |
174 | return err | |
175 | } | |
176 | req.Handlers.Build.PushBack(MakeAddToUserAgentFreeFormHandler("Waiter")) | |
177 | err = req.Send() | |
178 | ||
179 | // See if any of the acceptors match the request's response, or error | |
180 | for _, a := range w.Acceptors { | |
181 | if matched, matchErr := a.match(w.Name, w.Logger, req, err); matched { | |
182 | return matchErr | |
183 | } | |
184 | } | |
185 | ||
186 | // The Waiter should only check the resource state MaxAttempts times | |
187 | // This is here instead of in the for loop above to prevent delaying | |
188 | // unnecessary when the waiter will not retry. | |
189 | if attempt == w.MaxAttempts { | |
190 | break | |
191 | } | |
192 | ||
193 | // Delay to wait before inspecting the resource again | |
194 | delay := w.Delay(attempt) | |
195 | if sleepFn := req.Config.SleepDelay; sleepFn != nil { | |
196 | // Support SleepDelay for backwards compatibility and testing | |
197 | sleepFn(delay) | |
198 | } else if err := aws.SleepWithContext(ctx, delay); err != nil { | |
199 | return awserr.New(CanceledErrorCode, "waiter context canceled", err) | |
200 | } | |
201 | } | |
202 | ||
203 | return awserr.New(WaiterResourceNotReadyErrorCode, "exceeded wait attempts", nil) | |
204 | } | |
205 | ||
206 | // A WaiterAcceptor provides the information needed to wait for an API operation | |
207 | // to complete. | |
208 | type WaiterAcceptor struct { | |
209 | State WaiterState | |
210 | Matcher WaiterMatchMode | |
211 | Argument string | |
212 | Expected interface{} | |
213 | } | |
214 | ||
215 | // match returns if the acceptor found a match with the passed in request | |
216 | // or error. True is returned if the acceptor made a match, error is returned | |
217 | // if there was an error attempting to perform the match. | |
218 | func (a *WaiterAcceptor) match(name string, l aws.Logger, req *Request, err error) (bool, error) { | |
219 | result := false | |
220 | var vals []interface{} | |
221 | ||
222 | switch a.Matcher { | |
223 | case PathAllWaiterMatch, PathWaiterMatch: | |
224 | // Require all matches to be equal for result to match | |
225 | vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument) | |
226 | if len(vals) == 0 { | |
227 | break | |
228 | } | |
229 | result = true | |
230 | for _, val := range vals { | |
231 | if !awsutil.DeepEqual(val, a.Expected) { | |
232 | result = false | |
233 | break | |
234 | } | |
235 | } | |
236 | case PathAnyWaiterMatch: | |
237 | // Only a single match needs to equal for the result to match | |
238 | vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument) | |
239 | for _, val := range vals { | |
240 | if awsutil.DeepEqual(val, a.Expected) { | |
241 | result = true | |
242 | break | |
243 | } | |
244 | } | |
245 | case PathListWaiterMatch: | |
246 | // ignored matcher | |
247 | case StatusWaiterMatch: | |
248 | s := a.Expected.(int) | |
249 | result = s == req.HTTPResponse.StatusCode | |
250 | case ErrorWaiterMatch: | |
251 | if aerr, ok := err.(awserr.Error); ok { | |
252 | result = aerr.Code() == a.Expected.(string) | |
253 | } | |
254 | default: | |
255 | waiterLogf(l, "WARNING: Waiter %s encountered unexpected matcher: %s", | |
256 | name, a.Matcher) | |
257 | } | |
258 | ||
259 | if !result { | |
260 | // If there was no matching result found there is nothing more to do | |
261 | // for this response, retry the request. | |
262 | return false, nil | |
263 | } | |
264 | ||
265 | switch a.State { | |
266 | case SuccessWaiterState: | |
267 | // waiter completed | |
268 | return true, nil | |
269 | case FailureWaiterState: | |
270 | // Waiter failure state triggered | |
271 | return true, awserr.New(WaiterResourceNotReadyErrorCode, | |
272 | "failed waiting for successful resource state", err) | |
273 | case RetryWaiterState: | |
274 | // clear the error and retry the operation | |
275 | return false, nil | |
276 | default: | |
277 | waiterLogf(l, "WARNING: Waiter %s encountered unexpected state: %s", | |
278 | name, a.State) | |
279 | return false, nil | |
280 | } | |
281 | } | |
282 | ||
283 | func waiterLogf(logger aws.Logger, msg string, args ...interface{}) { | |
284 | if logger != nil { | |
285 | logger.Log(fmt.Sprintf(msg, args...)) | |
286 | } | |
287 | } |