waiter.go 8.8 KB

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