Repository navigation
fix(idempotency): avoid running durable executions twice - #5873
Merged
dreamorosi merged 3 commits intoNov 1, 2026
Merged
dreamorosi merged 3 commits into
dreamorosi merged 3 commits into
Conversation
A durable execution can be suspended past its first invocation, so the in-progress expiry taken from that invocation made its record look dead and let another execution with the same payload take it over. The replay branch also continued past any existing record, including a completed one. Durable executions now store no in-progress expiry and record their execution ARN as the owner of the record. An execution continues only with its own in-progress record, so a retry before the first checkpoint works too, and a completed record returns the stored result. Records without an owner, written by earlier versions, keep the previous replay behaviour. The owner attribute name is configurable through ownerAttr in both the DynamoDB and cache persistence layers, and the docs get a section on durable functions.
Contributor
|
Thanks @vishwakt! I ran the probe from #5867 against this branch on Lambda (Node.js 24,
One thing the deploy scenarios showed: a resumed durable invocation runs whatever code is on A couple of small suggestions:
|
Contributor
Author
|
Thanks @svozza for running the probe on both layers and the deploy scenarios, that's really reassuring. Good to know the ownerless fallback is reachable in practice because resumed invocations run on Both suggestions are addressed:
|
svozza
approved these changes
Nov 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A durable execution suspended past its first invocation kept the in-progress expiry of that invocation, so its record looked dead and another execution with the same payload could take it over. The replay branch also continued past any existing record, including another execution's lock or a completed result. This PR stores no in-progress expiry for durable executions and records the execution ARN as the owner of the record, so an execution only continues with its own record.
It also fixes a second bug: on
main, a crash before the first checkpoint fails the whole durable execution, because the retry runs inExecutionModeand is rejected by its own record.Changes
makeIdempotentpassescontext.executionContext.durableExecutionArnto the handler. Durable executions save their in-progress record without an in-progress expiry, andsaveInProgressno longer logs theregisterLambdaContextwarning when an owner is provided.IdempotencyRecordhas an optionalowner.saveInProgressandsaveSuccessaccept it through the existing options object ({ identity, owner }), so the owner stays on the completed record too.IdempotencyItemAlreadyExistsError. It continues only with anINPROGRESSrecord owned by the same execution. A completed record returns the stored result, and any other record is handled like before, for exampleIdempotencyAlreadyInProgressError.ReplayModeonce more than one operation is checkpointed. A retry after a crash before the first checkpoint runs inExecutionMode, and onmainit is rejected by its own record, failing the execution. It now continues with its own record.ownerAttr(defaultowner), added toPERSISTENCE_ATTRIBUTE_KEY_MAPPINGSand read and written by both the DynamoDB and cache persistence layers. No change was needed to their locking conditions, since both already treat a record without an in-progress expiry as locked until it expires.docs/features/idempotency.mdwith a snippet, the recommendation to setexpiresAfterSecondsto at least the execution timeout, a note on records that stay locked when an execution ends without cleanup, a note for custom persistence stores, andownerAttrin both attribute tables.examples/snippetsgets@aws/durable-execution-sdk-jsas a dev dependency so the snippet type-checks.CachePersistenceLayerand an in-memory client. Execution A ships the order and is suspended, B starts with the same payload after A's first invocation would have timed out and is rejected, A resumes and completes, and B then gets the stored result, with the work running once. Other tests cover a foreign lock during replay, own, foreign and ownerless completed records during replay, a retry before the first checkpoint, and the owner round trip in both layers. All the new tests fail againstmain.Issue number: closes #5867
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
Disclaimer: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.