-
Notifications
You must be signed in to change notification settings - Fork 7
Waiters #292
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Waiters #292
Changes from 17 commits
0f224ac
bbcb6cc
4da115b
33bf1da
ad95325
fd76a10
c6a9395
8f4856e
4b0a05d
3dd5e64
970f236
c588576
5f70fa1
ff32042
0c8cf8b
63c6e6f
d026d67
f28e317
01aca71
3ff3ca1
6decd06
779fe1a
e8e5f2b
4b64c83
dcfc31e
6da8bfb
b788ecd
416a74d
93cd13d
1fdd251
b01734d
7a6b4d2
f8493ed
6ef2dfd
df63769
e58f50c
874cf28
c7c81dd
470b0d3
dd11d0e
25394d5
108834b
835e376
3420325
70c3315
7ebe9b0
1334bbe
87185bc
61ada8c
01a9483
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require_relative 'waiters/errors' | ||
| require_relative 'waiters/poller' | ||
| require_relative 'waiters/waiter' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Smithy | ||
| module Client | ||
| module Waiters | ||
| module Errors | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Raised when a waiter detects a condition where the waiter can never | ||
| # succeed. | ||
| class WaiterFailed < StandardError; end | ||
|
|
||
| class FailureStateError < WaiterFailed | ||
| MSG = "stopped waiting, encountered a failure state" | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
|
|
||
| def initialize(response) | ||
| @response = response | ||
| super(MSG) | ||
| end | ||
|
|
||
| attr_reader :response | ||
| end | ||
|
|
||
| class MaxWaitTimeExceededError < WaiterFailed | ||
| MSG = "stopped waiting after maximum wait time of %s seconds was exceeded" | ||
|
|
||
| def initialize(max_wait_time) | ||
| super(MSG % [max_wait_time]) | ||
| end | ||
| end | ||
|
|
||
| class UnexpectedError < WaiterFailed | ||
| MSG = "stopped waiting due to an unexpected error: %s" | ||
|
|
||
| def initialize(error) | ||
| @error = error | ||
| super(MSG % [error.message]) | ||
| end | ||
|
|
||
| attr_reader :error | ||
| end | ||
|
|
||
| # Raised when attempting to get a waiter by name and the waiter has not | ||
| # been defined. | ||
| class NoSuchWaiterError < ArgumentError | ||
| MSG = "no such waiter" | ||
|
|
||
| def initialize | ||
| super(MSG) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Smithy | ||
| module Client | ||
| module Waiters | ||
| class Poller | ||
| def initialize(options = {}) | ||
| @operation_name = options[:operation_name] | ||
| @acceptors = options[:acceptors] | ||
| end | ||
|
|
||
| def call(client, params) | ||
| @input = params | ||
| begin | ||
| resp = client.send(@operation_name, params) | ||
| rescue StandardError => e | ||
| error = e | ||
| end | ||
| resp_or_error = resp || error | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| status = evaluate_acceptors(resp, error) | ||
| [resp_or_error, status] | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def evaluate_acceptors(resp, error) | ||
| @acceptors.each do |acceptor| | ||
| return acceptor['state'] if acceptor_matches?(acceptor['matcher'], resp, error) | ||
| end | ||
|
|
||
| # If none of the acceptors match and an error was encountered, | ||
| # transition to failure state. Otherwise, transition to retry state. | ||
| if error | ||
| 'error' | ||
| else | ||
| 'retry' | ||
| end | ||
| end | ||
|
|
||
| def acceptor_matches?(matcher, resp, error) | ||
| matcher_type = matcher.keys[0] | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| send("matches_#{matcher_type}?", matcher[matcher_type], resp, error) | ||
| end | ||
|
|
||
| def matches_output?(path_matcher, resp, error) | ||
| return false unless error.nil? | ||
|
|
||
| actual = JMESPath.search(underscore_jmespath(path_matcher['path']), resp) | ||
| is_equal?(actual, path_matcher['expected'], path_matcher['comparator']) | ||
| end | ||
|
|
||
| def matches_inputOutput?(path_matcher, resp, error) | ||
| return false unless error.nil? && @input | ||
|
|
||
| data = { | ||
| input: @input, ### Where do we get this? | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| output: resp | ||
| } | ||
| actual = JMESPath.search(underscore_jmespath(path_matcher['path']), data) | ||
| is_equal?(actual, path_matcher['expected'], path_matcher['comparator']) | ||
| end | ||
|
|
||
| def matches_success?(path_matcher, resp, error) | ||
| if path_matcher == true | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| !resp.nil? | ||
| else | ||
| !error.nil? | ||
| end | ||
| end | ||
|
|
||
| def matches_errorType?(path_matcher, resp, error) | ||
| return false unless resp.nil? | ||
|
|
||
| err = path_matcher.split('#').last.split('#').first | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| error.class.to_s.include?(err) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't they be equal here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At least when I was testing the actual error raised will include namespaces too (e.g. actual error is
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is the same preprocessing problem right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm preprocessing would ensure the expected error would always be |
||
| end | ||
|
|
||
| def is_equal?(actual, expected, comparator) | ||
| case comparator | ||
| when 'stringEquals' | ||
| return actual == expected | ||
| when 'booleanEquals' | ||
| return actual.to_s == expected | ||
| when 'allStringEquals' | ||
| return false if actual.nil? || actual.empty? | ||
|
|
||
| actual.each do |value| | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| return false if value != expected | ||
| end | ||
| return true | ||
| when 'anyStringEquals' | ||
| return false if actual.nil? || actual.empty? | ||
|
|
||
| actual.each do |value| | ||
| return true if value == expected | ||
| end | ||
| return false | ||
| end | ||
| end | ||
|
|
||
| def underscore(string) | ||
| string.gsub(/::/, '/') | ||
| .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2') | ||
| .gsub(/([a-z\d])([A-Z])/,'\1_\2') | ||
| .tr("-", "_") | ||
| .downcase | ||
| end | ||
|
|
||
| def underscore_jmespath(expression) | ||
| expression | ||
| .gsub(' or ', '||') | ||
| .gsub(/(?<![`'])\b\w+\b(?![`'])/) { |str| underscore(str) } | ||
| end | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Smithy | ||
| module Client | ||
| module Waiters | ||
| class Waiter | ||
| def initialize(options = {}) | ||
| unless options[:max_wait_time].is_a?(Integer) | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| raise ArgumentError, 'Waiter must be initialized with `:max_wait_time`' | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| end | ||
|
|
||
| @max_wait_time = options[:max_wait_time] | ||
| @remaining_time = @max_wait_time | ||
| @min_delay = options[:min_delay] | ||
| @max_delay = options[:max_delay] | ||
| if @max_delay < 1 | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| raise ArgumentError, '`:max_delay` must be greater than 0' | ||
| end | ||
| if @min_delay < 1 || @min_delay > @max_delay | ||
| raise ArgumentError, '`:min_delay` must be greater than 0 and less than or equal to `:max_delay`' | ||
| end | ||
|
|
||
| @poller = options[:poller] | ||
| @client = options[:client] # runtime approach | ||
| end | ||
|
|
||
| def wait(client, params) | ||
| poll(client, params) | ||
| end | ||
|
|
||
| # Used for runtime approach | ||
| def wait_custom(params) | ||
| poll(@client, params) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def poll(client, params) | ||
| attempts = 0 | ||
| loop do | ||
| resp, status = @poller.call(client, params) | ||
| attempts += 1 | ||
|
|
||
| case status.to_sym | ||
|
richardwang1124 marked this conversation as resolved.
Outdated
|
||
| when :retry | ||
| when :success then return resp | ||
| when :failure then raise Errors::FailureStateError.new(resp) | ||
| when :error then raise Errors::UnexpectedError.new(resp) | ||
| end | ||
|
|
||
| raise Errors::MaxWaitTimeExceededError.new(@max_wait_time) if @remaining_time == 0 | ||
|
|
||
| delay = delay(attempts) | ||
| @remaining_time -= delay | ||
| sleep(delay) | ||
| end | ||
| end | ||
|
|
||
| def delay(attempts) | ||
| attempt_ceiling = (Math.log(@max_delay / @min_delay) / Math.log(2)) + 1 | ||
| delay = attempts > attempt_ceiling ? @max_delay : @min_delay * 2 ** (attempts - 1) | ||
| delay = rand(@min_delay..delay) | ||
| delay = @remaining_time if @remaining_time - delay <= @min_delay | ||
| delay | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # This is generated code! | ||
|
|
||
| require 'smithy-client/waiters' | ||
|
|
||
| module <%= module_name %> | ||
| # @api private | ||
| module Waiters | ||
| <% waiters.each do |waiter| -%> | ||
| # @api private | ||
| class <%= waiter.name %> | ||
| def initialize(options = {}) | ||
| @client = options[:client] | ||
| @waiter = Smithy::Client::Waiters::Waiter.new( | ||
| max_wait_time: options[:max_wait_time], | ||
| min_delay: options[:min_delay] || <%= waiter.min_delay %>, | ||
| max_delay: options[:max_delay] || <%= waiter.max_delay %>, | ||
| poller: Smithy::Client::Waiters::Poller.new( | ||
| operation_name: :<%= waiter.operation_name %>, | ||
| <%= waiter.acceptors %> | ||
| ) | ||
| ) | ||
| end | ||
|
|
||
| def wait(params = {}) | ||
| @waiter.wait(@client, params) | ||
| end | ||
| end | ||
|
|
||
| <% end -%> | ||
| end | ||
| end |
Uh oh!
There was an error while loading. Please reload this page.