Skip to content

Implement updated standard retry behavior#733

Open
Alan4506 wants to merge 2 commits into
smithy-lang:developfrom
Alan4506:retries-2.1-sep-update-v2
Open

Implement updated standard retry behavior#733
Alan4506 wants to merge 2 commits into
smithy-lang:developfrom
Alan4506:retries-2.1-sep-update-v2

Conversation

@Alan4506

@Alan4506 Alan4506 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description of changes:

Updates the standard retry behavior:

  • Throttling backoff: throttling errors use a larger base backoff (1000ms) than other errors (50ms), based on error.is_throttling_error.
  • Retry quota: RETRY_COST is now 14, throttling retries cost 5, and the timeout-specific cost is removed.
  • DynamoDB defaults: DynamoDB / DynamoDB Streams clients default to 4 max attempts and a 25ms base backoff, applied per-value (a value configured by the customer always wins). The generic runtime stays AWS-agnostic; the DynamoDB knowledge lives in an AWS codegen integration.
  • Long-polling: long-polling operations (SQS ReceiveMessage, SFN GetActivityTask, SWF PollForActivityTask/PollForDecisionTask) back off before giving up even when the quota is exhausted. Detection checks the smithy.api#longPoll trait with a hardcoded fallback for these operations.
  • x-amz-retry-after: honored when returned by the service, capped at 5 seconds above the normal backoff; invalid/missing values fall back to normal backoff and the standard HTTP Retry-After is ignored.

Testing:

Added unit tests for each change (throttling vs non-throttling backoff, quota costs, DynamoDB per-value defaults, the long-polling backoff matrix, and x-amz-retry-after parsing/clamping/wiring). Also updated affected functional tests. All Python checks pass (make check-py, make test-py) and the Java codegen builds successfully.

Follow-up:

The awsJson protocol's _create_error is not on develop yet (it lives on the json-rpc branch). Once that merges, it needs the same parse_retry_after(response) wiring that awsQuery already has, so x-amz-retry-after is honored for awsJson services (e.g. DynamoDB) too.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@Alan4506 Alan4506 requested a review from a team as a code owner July 2, 2026 16:07
*/
public final class AwsLongPollingIntegration implements PythonIntegration {

private static final Map<String, Set<String>> LONG_POLLING_OPERATIONS = Map.of(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Question, maybe blocking?]

Does it really make sense for us to do this now before the trait is ready? I'd rather not have to track the separate work of updating this generator to function based on the trait and just update it when the rest of the SDKs do.

If we are already committed to shipping this, it's fine, but it is extra effort on our part for no customer benefit.

@Alan4506 Alan4506 Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

After a deeper look into other SDKs, I think it makes more sense to keep long polling in this PR, implemented as trait-first with a hardcoded fallback:

if (operation.hasTrait(LongPollTrait.class)) {
    return true;
}
return HARDCODED_LONG_POLLING_OPERATIONS...  // SQS/SFN/SWF
  • The smithy.api#longPoll trait shipped in Smithy 1.69 (Add a long-poll trait smithy#3019), so hasTrait(LongPollTrait) compiles and works today. The service models don't carry the trait yet, so it currently falls through to the hardcoded table.
  • This trait-first + fallback method matches some other Smithy SDKs, such as Go, Kotlin, and Swift. They all check the trait first and fall back to a hardcoded table.
  • Once the service models carry the trait, we can just delete the fallback table.

endpoint_resolver=config.endpoint_resolver,
retry_strategy=retry_strategy,
retry_strategy=retry_strategy,${?isLongPolling}
is_long_polling=True,${/isLongPolling}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[blocking, change requested]

This information is retry specific. Plumbing it through every operation call works fine in this instance, but this type of solution isn't scalable. This method signature would grow very quickly if we needed to update the pipeline method's signature for every branching decision that we make as part of the pipeline call.

We should talk about the right way to plumb through the information. We already have the APIOperation object in scope. Since this will eventually be a modeled trait, doesn't it make more sense for us to add it to the operation object as opposed to growing the pipeline method's signature?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call. Dropped is_long_polling from the pipeline signature entirely. The long-polling flag is now carried on the operation context (smithy_core.retries.LONG_POLLING, a PropertyKey[bool]) instead of a dedicated parameter. The generator only emits the key on operations that need it.

retry_delay = self.backoff_strategy.compute_next_backoff_delay(0)
return StandardRetryToken(retry_count=0, retry_delay=retry_delay)
return StandardRetryToken(
retry_count=0, retry_delay=retry_delay, is_long_polling=is_long_polling

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Change requested, blocking but open to discussion]

The concept of a token represents the state of a given attempt. This concept is unrelated to whether or not the operation is used for long polling.

Plumbing this through the retry token means that refresh_retry_token_for_retry needs to copy it forward unchanged. This is more boilerplate and more complex than it needs to be.

I'm trying to think of a better alternative. Off the top of my head, we could add the operation's schema to these calls, maybe via a retries_context like concept? Or just put this long_polling in a context so that it can grow without growing the method signature?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and this is the same change as above. StandardRetryToken.is_long_polling is gone, so refresh_retry_token_for_retry no longer copies it forward. acquire_initial_retry_token and refresh_retry_token_for_retry now take an optional context. They read LONG_POLLING from it to decide whether the operation should still back off when the retry quota is exhausted.

non-throttling errors when the customer did not configure one. Only applies
to standard mode.
"""
self._default_max_attempts = default_max_attempts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Question/discussion]

Did you consider making this a service-level interceptor instead of passing it in here? The interceptor could set these values if they're unset and only apply to DDB. I think that's more inline with the intention of the library's original authors.

You may need to wait until we have provenance tracking to support this though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call. I agree that a service-level interceptor can be a reasonable refactor in the future, once we have provenance tracking. I can make a follow-up for it.


_LOGGER = logging.getLogger(__name__)

_RETRY_AFTER_HEADER = "x-amz-retry-after"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Blocking]

We should not add aws-specific logic to smithy-http or other generic non-AWS specific code. We need to find a way to hook into this from the AWS specific code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The x-amz-retry-after parsing moved to smithy_aws_core.utils.parse_retry_after. smithy-http now only has a neutral _retry_after hook that returns None, which the AWS protocols override.

@Alan4506 Alan4506 requested a review from a team as a code owner July 14, 2026 20:02
@Alan4506 Alan4506 force-pushed the retries-2.1-sep-update-v2 branch from e5e0c7d to 18d19e6 Compare July 14, 2026 21:19
@Alan4506 Alan4506 force-pushed the retries-2.1-sep-update-v2 branch from 18d19e6 to a900e8c Compare July 14, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants