Skip to content
This repository has been archived by the owner on Nov 20, 2018. It is now read-only.

Commit

Permalink
Reworked waiters to support v2 definitions.
Browse files Browse the repository at this point in the history
Rewrote the internals to support the waiters v2 format.
This new format allows for multi success and multiple failure
states. It is also a much simpler format to work with.
  • Loading branch information
trevorrowe committed Jan 12, 2015
1 parent e2b7ec8 commit d53ba2f
Show file tree
Hide file tree
Showing 12 changed files with 428 additions and 313 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
Unreleased Changes
------------------

* Feature - Waiters - Added support for version 2 of the waiters
definition format.

* Issue - Pagingation - Resolved an issue where an empty hashes or arrays
were being treated as valid next tokens for paging response. This appears
to only have affected `Aws::DynamoDB::Client#batch_get_item`.
Expand Down
1 change: 1 addition & 0 deletions aws-sdk-core/lib/aws-sdk-core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ module Signers
end

module Waiters
autoload :Poller, 'aws-sdk-core/waiters/poller'
autoload :Errors, 'aws-sdk-core/waiters/errors'
autoload :NullProvider, 'aws-sdk-core/waiters/null_provider'
autoload :Provider, 'aws-sdk-core/waiters/provider'
Expand Down
6 changes: 0 additions & 6 deletions aws-sdk-core/lib/aws-sdk-core/api/service_customizations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,6 @@ def apply_protocol_plugin(client_class)
# parsing of the output
client_class.api.operation(:get_bucket_location).
instance_variable_set("@output", nil)

defs = client_class.waiters.instance_variable_get("@definitions")
defs[:bucket_exists]['ignore_errors'] = ['NotFound']
defs[:object_exists]['ignore_errors'] = ['NotFound']
defs[:bucket_not_exists]['success_value'] = 'NotFound'
defs[:object_not_exists]['success_value'] = 'NotFound'
end

customize 'sqs' do
Expand Down
115 changes: 65 additions & 50 deletions aws-sdk-core/lib/aws-sdk-core/client_waiters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,76 +23,91 @@ def waiters
end
end

# Waits until a particular condition is satisfied. This works by
# polling a client request and checking for particular response
# data or errors. Waiters each have a default duration max attempts
# which are configurable. Additionally, you can register callbacks
# and stop waiters by throwing `:success` or `:failure`.
#
# @example Basic usage
# client.wait_until(:waiter_name)
#
# @example Configuring interval and maximum attempts
# client.wait_until(:waiter_name) do |w|
# w.interval = 10 # number of seconds to sleep between attempts
# w.max_attempts = 6 # maximum number of polling attempts
# end
# Waiters polls an API operation until a resource enters a desired
# state.
#
# @example Rescuing a failed wait
# begin
# client.wait_until(:waiter_name)
# rescue Aws::Waiters::Errors::WaiterFailed
# # gave up waiting
# end
# ## Basic Usage
#
# @example Waiting with progress callbacks
# client.wait_until(:waiter_name) do |w|
# Waiters will poll until they are succesful, they fail by
# entering a terminal state, or until a maximum number of attempts
# are made.
#
# # yields just before polling for change
# w.before_attempt do |attempt|
# # attempts - number of previous attempts made
# end
# # polls in a loop, sleeping between attempts
# client.waiter_until(waiter_name, params)
#
# ## Configuration
#
# # yields before sleeping
# w.before_wait do |attempt, prev_response|
# # attempts - number of previous attempts made
# # prev_response - the last client response received
# You can configure the maximum number of polling attempts, and the
# delay (in seconds) between each polling attempt. You configure
# waiters by passing a block to #{wait_until}:
#
# # poll for ~25 seconds
# client.wait_until(...) do |w|
# w.max_attempts = 5
# w.delay = 5
# end
# end
#
# @example Throw :success or :failure to terminate early
# # wait for an hour, not for a number of requests
# client.wait_until(:waiter_name) do |waiter|
# one_hour = Time.now + 3600
# waiter.max_attempts = nil
# waiter.before_attempt do |attempt|
# throw(:failure, 'waited to long') if Time.now > one_hour
# ## Callbacks
#
# You can be notified before each polling attempt and before each
# delay. If you throw `:success` or `:failure` from these callbacks,
# it will terminate the waiter.
#
# started_at = Time.now
# client.wait_until(...) do |w|
#
# # disable max attempts
# w.max_attempts = nil
#
# # poll for 1 hour, instead of a number of attempts
# before_wait do |attempts, response|
# throw :failure if Time.now - started_at > 3600
# end
#
# end
#
# ## Handling Errors
#
# When a waiter is successful, it returns `true`. When a waiter
# fails, it raises an error. **All errors raised extend from
# {Aws::Waiters::Errors::WaiterFailed}**.
#
# begin
# client.wait_until(...)
# rescue Aws::Waiters::Errors::WaiterFailed
# # resource did not enter the desired state in time
# end
#
# @param [Symbol] waiter_name The name of the waiter. See {#waiter_names}
# for a full list of supported waiters.
#
# @param [Hash] params Additional request parameters. See the {#waiter_names}
# for a list of supported waiters and what request they call. The
# called request determines the list of accepted parameters.
# @return [Seahorse::Client::Response] Returns the client response from
# the successful polling request. If `:success` is thrown from a callback,
# then the 2nd argument to `#throw` is returned.
# @yieldparam [Waiters::Waiter] waiter Yields a {Waiters::Waiter Waiter}
#
# @yieldparam [Waiters::Waiter] waiter Yields a {Waiters::Waiter Waiter}
# object that can be configured prior to waiting.
# @raise [Waiters::Errors::NoSuchWaiter] Raised when the named waiter
# is not defined.
# @raise [Waiters::Errors::WaiterFailed] Raised when one of the
# following conditions is met:
#
# * A failure condition is detected
# * The maximum number of attempts has been made without success
# * `:failure` is thrown from a callback
# @raise [Errors::FailureStateError] Raised when the waiter terminates
# because the waiter has entered a state that it will not transition
# out of, preventing success.
#
# @raise [Errors::TooManyAttemptsError] Raised when the configured
# maximum number of attempts have been made, and the waiter is not
# yet successful.
#
# @raise [Errors::UnexpectedError] Raised when an error is encounted
# while polling for a resource that is not expected.
#
# @raise [Errors::NoSuchWaiterError] Raised when you request to wait
# for an unknown state.
#
# @return [Boolean] Returns `true` if the waiter was successful.
#
def wait_until(waiter_name, params = {}, &block)
waiter = self.class.waiters.waiter(waiter_name)
yield(waiter) if block_given?
waiter.wait(self, params)
waiter.wait(client:self, params:params)
end

# Returns the list of supported waiters.
Expand Down
1 change: 1 addition & 0 deletions aws-sdk-core/lib/aws-sdk-core/emr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
api: File.join(Aws::API_DIR, 'EMR.api.json'),
docs: File.join(Aws::API_DIR, 'EMR.docs.json'),
paginators: File.join(Aws::API_DIR, 'EMR.paginators.json'),
waiters: File.join(Aws::API_DIR, 'EMR.waiters.json'),
})
4 changes: 4 additions & 0 deletions aws-sdk-core/lib/aws-sdk-core/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ class ServiceError < RuntimeError
# @param [Seahorse::Client::RequestContext] context
# @param [String] message
def initialize(context, message)
@code = self.class.code
@context = context
super(message)
end

# @return [String]
attr_reader :code

# @return [Seahorse::Client::RequestContext] The context of the request
# that triggered the remote service to return this error.
attr_reader :context
Expand Down
1 change: 1 addition & 0 deletions aws-sdk-core/lib/aws-sdk-core/kinesis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
api: File.join(Aws::API_DIR, 'Kinesis.api.json'),
docs: File.join(Aws::API_DIR, 'Kinesis.docs.json'),
paginators: File.join(Aws::API_DIR, 'Kinesis.paginators.json'),
waiters: File.join(Aws::API_DIR, 'Kinesis.waiters.json'),
})
62 changes: 50 additions & 12 deletions aws-sdk-core/lib/aws-sdk-core/waiters/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,61 @@ module Errors
# succeed.
class WaiterFailed < StandardError; end

class FailureStateError < WaiterFailed

MSG = "stopped waiting, encountered a failure state"

def initialize(response)
@response = response
super(MSG)
end

# @return [Seahorse::Client::Response] The response that matched
# the failure state.
attr_reader :response

end

class TooManyAttemptsError < WaiterFailed

MSG = "stopped waiting after %d attempts without success"

def initialize(attempts)
@attempts = attempts
super(MSG % [attempts])
end

# @return [Integer]
attr_reader :attempts

end

class UnexpectedError < WaiterFailed

MSG = "stopped waiting due to an unexpected error: %s"

def initialize(error)
@error = error
super(MSG % [error.message])
end

# @return [Exception] The unexpected error.
attr_reader :error

end

# Raised when attempting to get a waiter by name and the waiter has not
# been defined.
class NoSuchWaiter < ArgumentError
class NoSuchWaiterError < ArgumentError

MSG = "no such waiter %s; valid waiter names are: %s"

def initialize(waiter_name, waiter_names)
msg = "no definition found for #{waiter_name.inspect}"
msg << "; valid waiter names are:"
waiter_names.sort.each.with_index do |name, n|
if n % 3 == 0
msg << "\n #{name.inspect}"
else
msg << ", #{name.inspect}"
end
end
super(msg)
waiter_names = waiter_names.map(&:inspect).join(', ')
super(MSG % [waiter_name.inspect, waiter_names])
end
end

end
end
end
end
102 changes: 102 additions & 0 deletions aws-sdk-core/lib/aws-sdk-core/waiters/poller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
module Aws
module Waiters

# Polls a single API operation inspecting the response data and/or error
# for states matching one of its acceptors.
# @api private
class Poller

# @api private
RAISE_HANDLER = Seahorse::Client::Plugins::RaiseResponseErrors::Handler

# @api private
def initialize(options = {})
@operation_name = underscore(options['operation']).to_sym
@acceptors = options['acceptors'] || []
end

# Makes an API call, returning the resultant state and the response.
#
# * `:success` - A success state has been matched.
# * `:failure` - A terminate failure state has been matched.
# * `:retry` - The waiter may be retried.
# * `:error` - The waiter encountered an un-expected error.
#
# @example A trival (bad) example of a waiter that polls indefinetly.
#
# loop do
#
# state, resp = poller.call(client:client, params:{})
#
# case state
# when :success then return true
# when :failure then return false
# when :retry then next
# when :error then raise 'oops'
# end
#
# end
#
# @option options [required,Client] :client
# @option options [required,Hash] :params
# @return [Array<Symbol,Response>]
def call(options = {})
response = send_request(options)
@acceptors.each do |acceptor|
if acceptor_matches?(acceptor, response)
return [acceptor['state'].to_sym, response]
end
end
[response.error ? :error : :retry, response]
end

private

def send_request(options)
req = options[:client].build_request(@operation_name, options[:params])
req.handlers.remove(RAISE_HANDLER)
req.send_request
end

def acceptor_matches?(acceptor, response)
send("matches_#{acceptor['matcher']}?", acceptor, response)
end

def matches_path?(acceptor, response)
JMESPath.search(path(acceptor), response.data) == acceptor['expected']
end

def matches_pathAll?(acceptor, response)
values = JMESPath.search(path(acceptor), response.data)
Array === values &&
values.count > 0 &&
values.all? { |value| value == acceptor['expected'] }
end

def matches_pathAny?(acceptor, response)
values = JMESPath.search(path(acceptor), response.data)
Array === values &&
values.count > 0 &&
values.any? { |value| value == acceptor['expected'] }
end

def matches_status?(acceptor, response)
response.context.http_response.status_code == acceptor['expected']
end

def matches_error?(acceptor, response)
Aws::Errors::ServiceError === response.error &&
response.error.code == acceptor['expected']
end

def path(acceptor)
acceptor['argument'].gsub(/\w+/) { |s| Seahorse::Util.underscore(s) }
end

def underscore(str)
Seahorse::Util.underscore(str)
end

end
end
end
Loading

1 comment on commit d53ba2f

@mtdowling
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎆

Please sign in to comment.