-
Notifications
You must be signed in to change notification settings - Fork 7
feat: Retry alignment #343
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
Changes from all commits
04fabf0
d334a0a
4e7f834
171f88c
252d943
26860b3
8e50860
26bb0a6
6a26c83
15e634e
a78969a
0555d60
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 |
|---|---|---|
|
|
@@ -6,66 +6,30 @@ module Plugins | |
| # @api private | ||
| class RetryErrors < Plugin | ||
| option( | ||
| :retry_strategy, | ||
| :retry_mode, | ||
|
jterapin marked this conversation as resolved.
|
||
| default: 'standard', | ||
| doc_default: "'standard'", | ||
| doc_type: 'String, Class', | ||
| doc_type: String, | ||
| docstring: <<~DOCS) | ||
| The retry strategy to use when retrying errors. This can be one of the following: | ||
| * `standard` - A standardized retry strategy used by the AWS SDKs. This includes support | ||
| for retry quotas, which limit the number of unsuccessful retries a client can make. | ||
| * `adaptive` - An experimental retry strategy that includes all the functionality of the | ||
| `standard` strategy along with automatic client side throttling. This is a provisional | ||
| strategy that may change behavior in the future. | ||
| * Any instance of a class that implements the following methods: | ||
| - `acquire_initial_retry_token(token_scope)` | ||
| - `refresh_retry_token(retry_token, error_info)` | ||
| - `record_success(retry_token)` | ||
| Specifies which retry algorithm to use. Values are: | ||
|
|
||
| * `standard` - A standardized set of retry rules across the Smithy-based SDKs. | ||
| This includes support for retry quotas, which limit the number of | ||
| unsuccessful retries a client can make. This is the default | ||
| value if no retry mode is provided. | ||
|
|
||
| * `adaptive` - A retry mode that includes all the functionality of | ||
| `standard` mode along with automatic client side throttling. | ||
| DOCS | ||
|
|
||
| option( | ||
| :retry_max_attempts, | ||
| :max_attempts, | ||
| default: 3, | ||
| doc_type: Integer, | ||
| docstring: <<~DOCS) | ||
| The maximum number attempts that will be made for a single request, including | ||
| the initial attempt. Used in the `standard` and `adaptive` retry strategies. | ||
| DOCS | ||
|
|
||
| option( | ||
| :retry_max_delay, | ||
| default: 20, | ||
| docstring: <<~DOCS) | ||
| The maximum delay, in seconds, between retry attempts. This option is ignored | ||
| if a custom `retry_backoff` is provided. Used in the `standard` and `adaptive` | ||
| retry strategies. | ||
| DOCS | ||
|
|
||
| option( | ||
| :retry_base_delay, | ||
| default: 2, | ||
| docstring: <<~DOCS) | ||
| The base delay, in seconds, used to calculate the exponential backoff for | ||
| retry attempts. This option is ignored if a custom `retry_backoff` is provided. | ||
| Used in the `standard` and `adaptive` retry strategies. | ||
| the initial attempt. | ||
| DOCS | ||
|
|
||
| option( | ||
| :retry_backoff, | ||
| doc_default: 'Smithy::Client::Retry::ExponentialBackoff.new', | ||
| rbs_type: 'Smithy::Client::Retry::ExponentialBackoff', | ||
| doc_type: '#call(attempts)', | ||
| docstring: <<~DOCS) do |config| | ||
| A callable object that calculates a backoff delay for a retry attempt. The callable | ||
| should accept a single argument, `attempts`, that represents the number of attempts | ||
| that have been made. Used in the `standard` and `adaptive` retry strategies. | ||
| DOCS | ||
| Retry::ExponentialBackoff.new( | ||
| retry_base_delay: config.retry_base_delay, | ||
| retry_max_delay: config.retry_max_delay | ||
| ) | ||
| end | ||
|
|
||
| option( | ||
| :adaptive_retry_wait_to_fill, | ||
| default: true, | ||
|
|
@@ -76,24 +40,54 @@ class RetryErrors < Plugin | |
| not retry instead of sleeping. | ||
| DOCS | ||
|
|
||
| option( | ||
| :retry_strategy, | ||
| doc_type: 'Object', | ||
| docstring: <<~DOCS) | ||
| The retry strategy used by the client. If not provided, a default strategy is built | ||
| based on `:retry_mode` — either `Standard` or `Adaptive`. | ||
|
|
||
| A custom strategy must respond to: | ||
|
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. Nice! |
||
| * `#acquire_initial_retry_token` - returns a token | ||
| * `#refresh_retry_token(token, error_info)` - returns a token or nil | ||
| * `#record_success(token)` - records a successful request | ||
| * `#request_bookkeeping(error_info = nil)` - updates internal state | ||
| DOCS | ||
|
|
||
| REQUIRED_STRATEGY_METHODS = %i[ | ||
| acquire_initial_retry_token | ||
| refresh_retry_token | ||
| record_success | ||
| request_bookkeeping | ||
| ].freeze | ||
|
|
||
| def after_initialize(client) | ||
| config = client.config | ||
| config.retry_strategy = | ||
| case config.retry_strategy | ||
| when 'standard' | ||
| Retry::Standard.new( | ||
| max_attempts: config.retry_max_attempts, | ||
| backoff: config.retry_backoff | ||
| ) | ||
| when 'adaptive' | ||
| Retry::Adaptive.new( | ||
| max_attempts: config.retry_max_attempts, | ||
| backoff: config.retry_backoff, | ||
| wait_to_fill: config.adaptive_retry_wait_to_fill | ||
| ) | ||
| else | ||
| config.retry_strategy | ||
| end | ||
| if config.retry_strategy | ||
| validate_strategy(config.retry_strategy) | ||
| else | ||
| config.retry_strategy = build_strategy(config) | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def validate_strategy(strategy) | ||
| missing = REQUIRED_STRATEGY_METHODS.reject { |m| strategy.respond_to?(m) } | ||
| return if missing.empty? | ||
|
|
||
| raise ArgumentError, "Custom retry_strategy must respond to: #{missing.join(', ')}" | ||
| end | ||
|
|
||
| def build_strategy(config) | ||
| case config.retry_mode | ||
| when 'standard' | ||
| Retry::Standard.new(max_attempts: config.max_attempts) | ||
| when 'adaptive' | ||
| Retry::Adaptive.new(max_attempts: config.max_attempts, wait_to_fill: config.adaptive_retry_wait_to_fill) | ||
| else | ||
| raise ArgumentError, "Must provide either 'standard' or 'adaptive' for retry_mode" | ||
| end | ||
| end | ||
|
|
||
| # @api private | ||
|
|
@@ -108,19 +102,35 @@ def call(context) | |
|
|
||
| def handle(context, retry_strategy, token) | ||
| response = track_feature(retry_strategy) { @handler.call(context) } | ||
| if (error = response.error) | ||
| return response unless retryable?(context.http_request) | ||
|
|
||
| error_info = Http::ErrorInspector.new(error, context.http_response) | ||
| token = retry_strategy.refresh_retry_token(token, error_info) | ||
| return response unless token | ||
| error_info = Http::ErrorInspector.new(response.error, context.http_response) if response.error | ||
| retry_strategy.request_bookkeeping(error_info) | ||
|
|
||
| Kernel.sleep(token.retry_delay) | ||
| else | ||
| unless response.error | ||
| retry_strategy.record_success(token) | ||
| return response | ||
| end | ||
| return response unless retryable?(context.http_request) | ||
|
|
||
| token = handle_error(context, retry_strategy, token, error_info) | ||
| return response unless token | ||
|
|
||
| retry_request(context, response, retry_strategy, token) | ||
| end | ||
|
|
||
| def handle_error(context, retry_strategy, token, error_info) | ||
| token = retry_strategy.refresh_retry_token(token, error_info) | ||
| return unless token | ||
|
|
||
| if token.no_retry_reason == :quota_exhausted | ||
| Kernel.sleep(token.retry_delay) if long_polling_operation?(context) | ||
| return | ||
| end | ||
|
|
||
| Kernel.sleep(token.retry_delay) | ||
| token | ||
| end | ||
|
|
||
| def retry_request(context, response, retry_strategy, token) | ||
| reset_request(context) | ||
| reset_response(context, response) | ||
| context.retries += 1 | ||
|
|
@@ -141,6 +151,11 @@ def reset_response(context, response) | |
| response.error = nil | ||
| end | ||
|
|
||
| # TODO: Revisit after trait is finalized. | ||
| def long_polling_operation?(context) | ||
| context.operation.traits.key?('smithy.api#longPoll') | ||
|
Collaborator
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. Not sure how this is gonna look, but at least according to the SEP this trait 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. Well, I should add a revisit task for this and ClockSkew on our board if there's isn't one already.
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. @richardwang1124 At least in Smithy website, it's under |
||
| end | ||
|
|
||
| def track_feature(retry_strategy, &block) | ||
| case retry_strategy | ||
| when Retry::Standard then Features.track('RETRY_MODE_STANDARD', &block) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,20 @@ def add_handlers(handlers, config) | |
| def after_initialize(client) | ||
|
Collaborator
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. Why is this removed? In V3 retries are disabled when stub_responses is true since customers need to mock the response behavior.
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. Instead of having this logic be here, I moved it off to
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 personally think keeping all the
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. I agree with keeping all |
||
| return unless client.config.stub_responses | ||
|
|
||
| remove_common_handlers(client) | ||
| remove_context_handlers(client) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def remove_common_handlers(client) | ||
| # Handlers removed when stubbing regardless of context. | ||
| # Subclasses should not override this method. | ||
| end | ||
|
|
||
| def remove_context_handlers(client) | ||
| # Context-specific handler removals. Override in subclasses | ||
| # to remove domain-specific handlers (e.g., domain-specific retry handler). | ||
| client.handlers.remove(RetryErrors::Handler) | ||
| end | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Juli might have more context, but should the class option be removed? I think it allowed customers to implement their own custom retry strategies, but not sure if we still want to support that or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SEP doesn't require custom retry strategy configuration & afaik specific configurable options exposed in V3's retry plugin also only apply to legacy. I definitely might be missing something but it seems V3 doesn't support fully customizable strategy either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In V3, a customer could pass their own custom
Procto influence how backoffs work but that is under the:legacyumbrella. We could have a way to accommodate this legacy support via migration gem.While SEP does not mention custom retries, I did see some references in Smithy docs: https://smithy.io/2.0/guides/client-guidance/retries.html
For customers who wants to NOT use migration gem BUT want custom retries to be a thing. They could opt to implement their own via duck typing our retries interface (aka below). Other ways are replacing this plugin with their own. Or even utilizing custom welds.
Note: I added more context in a separate comment below on what we need to do to resolve this.