Repository navigation
feat(smithy-core): auto-fill idempotency tokens before serialization - #913
Conversation
jonathan343
left a comment
There was a problem hiding this comment.
Thanks @rohangavankar! I think these changes mostly look good.
I think our code generator should detect members with this trait and append additional document to describe the behavior. Botocore adds This field is autopopulated if not provided.. We could add something similar?
|
I know we discussed the tradeoffs offline and decided on the serializer approach. After more thought, I think filling the token in the pipeline before serialization is better. Both work, but As you're aware, filling in the pipeline also:
It just needs to run after the whole It's a two-way door, but cheapest to change before release. Did you look into this any more after our discussion? |
9981a05 to
9d14e2b
Compare
847676b to
d97c886
Compare
| ) | ||
|
|
||
|
|
||
| async def test_unset_idempotency_token_is_generated_once( |
There was a problem hiding this comment.
Nit: I think a few of these could be merged:
test_unset_idempotency_token_is_generated_onceandtest_caller_input_is_not_mutated_by_token_fillcheck the same input and token.test_set_idempotency_token_is_sent_unchangedandtest_empty_string_idempotency_token_is_sent_unchangedcould be one test parametrized on "caller-token" and "".test_non_dataclass_input_is_passed_throughandtest_input_without_the_derived_field_is_passed_throughboth hit the "can't fill" path, but only the second checks the warning.
There was a problem hiding this comment.
Merged all three in 386e3be:
test_unset_idempotency_token_is_generated_once_on_a_copynow also checks the caller's input isn't mutated.test_set_idempotency_token_is_sent_unchangedis parametrized on"caller-token"and"".test_input_that_cannot_be_filled_is_passed_through_with_a_warningis parametrized on the non-dataclass input and the misnamed field, and checks the warning for both.
3fc6a99 to
386e3be
Compare
Operation inputs with an @idempotencyToken member now get a UUID v4 when the caller leaves the member unset. The fill runs once per call in the shared request pipeline, right after the whole modify_before_serialization interceptor chain. So read_before_execution and modify_before_serialization see the caller's own input, and an interceptor that returns a new input still gets a token. Serialization runs once per call, so every retry attempt sends the same token. A caller-supplied token is sent unchanged, and the caller's input object is never mutated: a generated token goes on a copy. Inputs that aren't dataclasses with the derived field, such as hand-written shapes, are passed through unchanged, and a warning is logged. Sync and async clients both get this, since they share the pipeline. The token generator is private. Callers who need deduplication across calls should pass their own token per call. A client-wide generator could not help there, since it still gives each new call a new token. botocore and the JS v3 SDK hard-code a UUID v4, and the PHP SDK only lets callers swap the random-byte source, so none of them exposes an option that returns the token itself. The Python field name of the token member is cached on APIOperation, so operations without a token pay one attribute read per call. The protocol test generator now pins the token for operations with an @idempotencyToken member by patching the private generator. That lets the QueryIdempotencyTokenAutoFill tests run for restJson1, restXml, awsQuery and ec2Query, so they come off the skip lists.
Generated docs for an @idempotencyToken member of an operation input now end with "This field is autopopulated if not provided.", matching botocore. Members without docs get that sentence alone. Tokens outside operation inputs are not filled by the client, so they get no note. The Java generators share CodegenUtils.getMemberDocs, used for both the .pyi stubs and eager models, and the Python generator applies the same rule. The regenerated protocol-test stubs pick up the note.
- Fold the caller-input mutation check into the unset-token test, since both used the same input and token. - Parametrize the caller-supplied token test on "caller-token" and "". - Parametrize the can't-fill test on a non-dataclass input and a misnamed dataclass field, so the warning is checked for both.
386e3be to
0c6f659
Compare
Summary
Operations with an idempotency token now get one filled in automatically when the caller leaves it unset. The request pipeline fills it with a UUID v4 right after the whole
modify_before_serializationinterceptor chain, before the request is serialized, so it works for every protocol whether the token goes in the body, a header or the query string. This matches smithy-java, which fills a copy inmodifyBeforeSerialization.The caller's input is never changed: the token goes on a shallow copy, so reusing one input across calls sends a new token each call. Retries resend the same token since the request is only serialized once. A token set by the caller or by an interceptor is sent as is, and an interceptor that returns a new input still gets a token. Interceptors from
read_before_serializationon, and logs, can see the generated token. Because the fill only happens in the pipeline, other serializations such asDocument.from_shape(input)and types-only packages never get a token. Hand-written inputs that aren't dataclasses with the token field are sent unchanged, with a warning.The generator is kept private, because a custom generator still can't dedupe across calls. Callers who need that should pass their own token.
This also turns on the idempotency token autofill protocol tests for restJson1, restXml, awsQuery and ec2Query.
Testing
Unit and protocol tests pass. I also generated a Secrets Manager client and ran it against the real service, where the token becomes the secret's version ID:
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.