Skip to content

Conversation

@ConnorKirk
Copy link
Contributor

@ConnorKirk ConnorKirk commented Dec 2, 2025

Issue number: fixes #7762

Summary

This PR adds support for durable functions in the Idempotency utility.

  1. Introduce the is_replay variable. We use this to determine if a function is being replayed.
  2. Correctly register the lambda context when a durable context is passed.

Changes

Please provide a summary of what's being changed

  • Add is_replay parameter to the handle method
  • If there's an existing idempotency record, but the function is a replay, then allow the function to continue
  • Add a stubbed DurableContext class
  • Add tests

User experience

Idempotency utility now allows durable functions to execute after suspension.

Notes

This leaves one issue remaining in the idempotency utility when used with durable functions.

We use context.getTimeInMillis() to calculate the expiry time of the INPROGRESS status. Duplicate payloads received after that expiry time will be allowed. In a durable execution, the length of the workflow can be up to 1 year, across multiple lambda invocations. This is longer than the maximum 15 minutes that an INPROGRESS status can currently have. Idempotency records for long running workflows will not be protected from duplicate invokes beyond the first 15 minutes of their life.

In future, we could

  1. Push for the inclusion of the DurableContext.getExecutionTimeoutInMillis() method. Once this is added, we can calculate the workflow timeout in the same manner we do for a single invocation.
  2. Allow the user to configure a timeout. We'd direct users to set this to match their durable execution timeout. I'm reluctant to add a new configuation option to the Handler/Config, especially if it might be redundant in the medium term (see Feature: Integrate custom metrics with new CloudWatch embedded metric #1)
  3. Retrieve the durable execution configuration within the function and set the timeout. This appears simpler, but requires an additional IAM permission, and might be surprising for a user.

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.

Durable Functions introduces the concept of function replays.
Previously, the idempotency utility would throw an
"IdempotencyItemAlreadyExistsError", as the replay has the same payload
of the initial execution. It appears as a duplicate, so is rejected.
Now, a replay is allowed
@ConnorKirk ConnorKirk requested a review from a team as a code owner December 2, 2025 18:11
@ConnorKirk ConnorKirk requested a review from sdangol December 2, 2025 18:11
@boring-cyborg boring-cyborg bot added tests typing Static typing definition related issues (mypy, pyright, etc.) labels Dec 2, 2025
@pull-request-size pull-request-size bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Dec 2, 2025
@ConnorKirk ConnorKirk changed the title Feat/df idempotency feat(idempotency): Allow durable functions to replay Dec 2, 2025
@github-actions github-actions bot added the feature New feature or functionality label Dec 2, 2025
@codecov
Copy link

codecov bot commented Dec 2, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.52%. Comparing base (0d97c70) to head (cb5ddca).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #7764   +/-   ##
========================================
  Coverage    96.52%   96.52%           
========================================
  Files          275      275           
  Lines        13123    13139   +16     
  Branches       990      992    +2     
========================================
+ Hits         12667    12683   +16     
  Misses         353      353           
  Partials       103      103           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@leandrodamascena leandrodamascena left a comment

Choose a reason for hiding this comment

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

Hey @ConnorKirk! Overall the PR looks good! I'd suggest considering these changes to use a Protocol instead of a concrete class - this avoids type conflicts with the real DurableContext from the Durable SDK and enables proper duck typing. I haven't fully tested with mypy, but we shouldn't have any issues.

If you think that make sense, you need to change the class name in all the other files.

Thanks a lot for working on this.

Comment on lines +98 to +108
class DurableContext:
_lambda_context: LambdaContext
_state: object

@property
def lambda_context(self) -> LambdaContext:
return self._lambda_context

@property
def state(self) -> object:
return self._state
Copy link
Contributor

Choose a reason for hiding this comment

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

Changed from a concrete class to a Protocol with @runtime_checkable. This avoids type conflicts with the real DurableContext from the Durable SDK - users can use the SDK's class directly without import collisions and we avoid creating a type only for this.

Suggested change
class DurableContext:
_lambda_context: LambdaContext
_state: object
@property
def lambda_context(self) -> LambdaContext:
return self._lambda_context
@property
def state(self) -> object:
return self._state
# Protocol enables duck typing without competing with the real DurableContext from the SDK
@runtime_checkable
class DurableContextProtocol(Protocol):
"""Protocol for durable execution context compatibility."""
@property
def lambda_context(self) -> LambdaContext:
"""The underlying Lambda context."""
...
@property
def state(self) -> object:
"""The durable execution state."""
...

Comment on lines +95 to +104
if hasattr(context, "state"):
# Extract lambda_context from DurableContext
durable_context = cast("DurableContext", context)
config.register_lambda_context(durable_context.lambda_context)
# Note: state.operations is accessed via duck typing at runtime
is_replay = len(durable_context.state.operations) > 1 # type: ignore[attr-defined]
else:
# Standard LambdaContext
config.register_lambda_context(context)
is_replay = False
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 if we change hasattr(context, "state") to hasattr(context, "state") and hasattr(context, "lambda_context"). We have more explicit check that ensures we only treat it as a durable context when both required attributes exist.

Suggested change
if hasattr(context, "state"):
# Extract lambda_context from DurableContext
durable_context = cast("DurableContext", context)
config.register_lambda_context(durable_context.lambda_context)
# Note: state.operations is accessed via duck typing at runtime
is_replay = len(durable_context.state.operations) > 1 # type: ignore[attr-defined]
else:
# Standard LambdaContext
config.register_lambda_context(context)
is_replay = False
# Check for durable context using duck typing
if hasattr(context, "state") and hasattr(context, "lambda_context"):
durable_context = cast("DurableContextProtocol", context)
config.register_lambda_context(durable_context.lambda_context)
# Note: state.operations is accessed via duck typing at runtime
is_replay = len(durable_context.state.operations) > 1 # type: ignore[attr-defined]
else:
# Standard LambdaContext
config.register_lambda_context(context)
is_replay = False

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or functionality size/L Denotes a PR that changes 100-499 lines, ignoring generated files. tests typing Static typing definition related issues (mypy, pyright, etc.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: add support for durable function in Idempotency Utility

3 participants