Skip to content

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

Merged
merged 50 commits into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
0f224ac
Add waiters to weather model
richardwang1124 Apr 24, 2025
bbcb6cc
Initial code generation for waiters
richardwang1124 Apr 24, 2025
4da115b
Add conditional templating for waiters
richardwang1124 Apr 24, 2025
33bf1da
Add initial Waiters module
richardwang1124 Apr 24, 2025
ad95325
Change casing
richardwang1124 Apr 24, 2025
fd76a10
Start adding waiter tests
richardwang1124 Apr 25, 2025
c6a9395
Small fixes
richardwang1124 Apr 28, 2025
8f4856e
Dynamically create waiters
richardwang1124 Apr 28, 2025
4b0a05d
Add dynamic waiter to code gen
richardwang1124 Apr 28, 2025
3dd5e64
Debug test failure
richardwang1124 Apr 28, 2025
970f236
Revert fixture model changes
richardwang1124 Apr 29, 2025
c588576
Start adding tests
richardwang1124 Apr 29, 2025
5f70fa1
Add more unit tests
richardwang1124 Apr 30, 2025
ff32042
Add more unit tests
richardwang1124 Apr 30, 2025
0c8cf8b
Update test names
richardwang1124 May 1, 2025
63c6e6f
Add underscore methods
richardwang1124 May 1, 2025
d026d67
Cleanup
richardwang1124 May 1, 2025
f28e317
Remove runtime trait waiters
richardwang1124 May 2, 2025
01aca71
Add changes from review comments
richardwang1124 May 2, 2025
3ff3ca1
Underscore paths during code generation
richardwang1124 May 2, 2025
6decd06
Add documentation and deprecated traits to generated waiters
richardwang1124 May 2, 2025
779fe1a
Refactor unit tests
richardwang1124 May 5, 2025
e8e5f2b
Add more tests
richardwang1124 May 5, 2025
4b64c83
Remove weather changes
richardwang1124 May 5, 2025
dcfc31e
Merge decaf
richardwang1124 May 5, 2025
6da8bfb
Build projections with waiters
richardwang1124 May 5, 2025
b788ecd
Rubocop and changes from PR comments
richardwang1124 May 6, 2025
416a74d
Clean up and shave down unit tests
richardwang1124 May 7, 2025
93cd13d
Remove extra error from spec
richardwang1124 May 7, 2025
1fdd251
Add wait_until documentation
richardwang1124 May 7, 2025
b01734d
Move waiters errors to smithy client errors
richardwang1124 May 7, 2025
7a6b4d2
Preprocess error type
richardwang1124 May 7, 2025
f8493ed
Use docstrings
richardwang1124 May 7, 2025
6ef2dfd
Remove waiters/errors
richardwang1124 May 7, 2025
df63769
Add documentation check
richardwang1124 May 7, 2025
e58f50c
Fix documentation check
richardwang1124 May 7, 2025
874cf28
Handle raise response error enabled and use stub responses for tests
richardwang1124 May 8, 2025
c7c81dd
Update wait_until to return nil upon success
richardwang1124 May 8, 2025
470b0d3
Cleanup fixture
richardwang1124 May 8, 2025
dd11d0e
Fix RBS errors
richardwang1124 May 9, 2025
25394d5
Add some fixes from PR comments
richardwang1124 May 19, 2025
108834b
Add rest of PR comment fixes
richardwang1124 May 20, 2025
835e376
Define variables inside context
richardwang1124 May 20, 2025
3420325
Make PR comment changes
richardwang1124 May 21, 2025
70c3315
Add todo
richardwang1124 May 21, 2025
7ebe9b0
Fix namespace issue with plugins
mullermp May 21, 2025
1334bbe
Merge branch 'decaf' into feature/waiters
mullermp May 21, 2025
87185bc
Rubocop
mullermp May 21, 2025
61ada8c
Fix TODO
mullermp May 21, 2025
01a9483
Remove fixed test
richardwang1124 May 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions gems/smithy-client/lib/smithy-client/waiters.rb
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'
44 changes: 44 additions & 0 deletions gems/smithy-client/lib/smithy-client/waiters/errors.rb
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

# 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"
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
94 changes: 94 additions & 0 deletions gems/smithy-client/lib/smithy-client/waiters/poller.rb
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)
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]
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)
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
!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
error.class.to_s.include?(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't they be equal here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 SampleClient1::Errors::MyError vs expected error MyError or smithy.ruby.tests#MyError).

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is the same preprocessing problem right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm preprocessing would ensure the expected error would always be MyError instead of smithy.ruby.tests#MyError but the actual error returned from the request would still be SampleClient1::Errors::MyError. Will it be guaranteed that the last part of the actual error after the :: delimiter will be equal to the expected error if it's supposed to match?

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
70 changes: 70 additions & 0 deletions gems/smithy-client/lib/smithy-client/waiters/waiter.rb
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)
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
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
Loading
Loading