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