-
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 26 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,44 @@ | ||
# frozen_string_literal: true | ||
|
||
module Smithy | ||
module Client | ||
module Waiters | ||
module Errors | ||
richardwang1124 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Raised when a waiter detects a condition where the waiter can never | ||
# succeed. | ||
class WaiterFailed < StandardError; end | ||
|
||
class FailureStateError < WaiterFailed | ||
def initialize(error) | ||
msg = "stopped waiting, encountered a failure state: %s" | ||
richardwang1124 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
super(msg % [error]) | ||
end | ||
end | ||
|
||
class MaxWaitTimeExceededError < WaiterFailed | ||
def initialize(max_wait_time) | ||
msg = "stopped waiting after maximum wait time of %s seconds was exceeded" | ||
super(msg % [max_wait_time]) | ||
end | ||
end | ||
|
||
class UnexpectedError < WaiterFailed | ||
def initialize(error) | ||
msg = "stopped waiting due to an unexpected error: %s" | ||
super(msg % [error]) | ||
end | ||
end | ||
|
||
# Raised when attempting to get a waiter by name and the waiter has not | ||
# been defined. | ||
class NoSuchWaiterError < ArgumentError | ||
def initialize(waiter_name, valid_waiters) | ||
msg = "no such waiter: %s; valid waiter names are: %s" | ||
super(msg % [waiter_name, valid_waiters]) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
# 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 | ||
output = client.send(@operation_name, params) | ||
richardwang1124 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
rescue StandardError => e | ||
error = e | ||
end | ||
output_or_error = output || error | ||
status = evaluate_acceptors(output, error) | ||
[output_or_error, status] | ||
end | ||
|
||
private | ||
|
||
def evaluate_acceptors(output, error) | ||
@acceptors.each do |acceptor| | ||
return acceptor['state'] if acceptor_matches?(acceptor['matcher'], output, error) | ||
end | ||
if error | ||
'error' | ||
else | ||
'retry' | ||
end | ||
end | ||
|
||
def acceptor_matches?(matcher, output, error) | ||
matcher_type = matcher.keys[0] | ||
richardwang1124 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
send("matches_#{matcher_type}?", matcher[matcher_type], output, error) | ||
end | ||
|
||
def matches_output?(path_matcher, output, error) | ||
return false unless error.nil? | ||
|
||
actual = JMESPath.search(path_matcher['path'], output) | ||
is_equal?(actual, path_matcher['expected'], path_matcher['comparator']) | ||
end | ||
|
||
def matches_inputOutput?(path_matcher, output, error) | ||
richardwang1124 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return false unless error.nil? && @input | ||
|
||
data = { | ||
input: @input, | ||
output: output | ||
} | ||
actual = JMESPath.search(path_matcher['path'], data) | ||
is_equal?(actual, path_matcher['expected'], path_matcher['comparator']) | ||
end | ||
|
||
def matches_success?(path_matcher, output, error) | ||
if path_matcher == true | ||
richardwang1124 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
!output.nil? | ||
else | ||
!error.nil? | ||
end | ||
end | ||
|
||
def matches_errorType?(path_matcher, output, error) | ||
return false unless output.nil? | ||
|
||
err = path_matcher.split('#').last.split('#').first | ||
richardwang1124 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
error.class.to_s.include?(err) | ||
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? 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 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? 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.all? { |value| value == expected } | ||
when 'anyStringEquals' | ||
return false if actual.nil? || actual.empty? | ||
|
||
actual.any? { |value| value == expected } | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# 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.
Show resolved
Hide resolved
|
||
raise ArgumentError, "expected `:max_wait_time` to be an integer, got: #{options[:max_wait_time]}" | ||
end | ||
|
||
@max_wait_time = options[:max_wait_time] | ||
@remaining_time = @max_wait_time | ||
@max_delay = max_delay(options[:max_delay]) | ||
@min_delay = min_delay(options[:min_delay]) | ||
@poller = options[:poller] | ||
end | ||
|
||
def wait(client, params) | ||
poll(client, params) | ||
end | ||
|
||
private | ||
|
||
def max_delay(delay) | ||
if delay < 1 | ||
raise ArgumentError, '`:max_delay` must be greater than 0' | ||
end | ||
delay | ||
end | ||
|
||
def min_delay(delay) | ||
if delay < 1 || delay > @max_delay | ||
raise ArgumentError, '`:min_delay` must be greater than 0 and less than or equal to `:max_delay`' | ||
end | ||
delay | ||
end | ||
|
||
def poll(client, params) | ||
attempts = 0 | ||
loop do | ||
output_or_error, status = @poller.call(client, params) | ||
attempts += 1 | ||
|
||
case status.to_sym | ||
richardwang1124 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
when :retry | ||
when :success then return output_or_error | ||
when :failure then raise Errors::FailureStateError.new(output_or_error) | ||
when :error then raise Errors::UnexpectedError.new(output_or_error) | ||
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 |
Uh oh!
There was an error while loading. Please reload this page.