Skip to content

feat(durabletask): implement timer origin and backwards-compatible optional timers#1733

Merged
dapr-bot merged 2 commits into
dapr:masterfrom
javier-aliaga:feat/implement-timer-origin
Apr 25, 2026
Merged

feat(durabletask): implement timer origin and backwards-compatible optional timers#1733
dapr-bot merged 2 commits into
dapr:masterfrom
javier-aliaga:feat/implement-timer-origin

Conversation

@javier-aliaga

@javier-aliaga javier-aliaga commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Description

Annotate every CreateTimerAction the SDK emits with the new origin oneof from durabletask-protobuf (CreateTimer / ExternalEvent / ActivityRetry / ChildWorkflowRetry), and emit a synthetic "optional" timer for waitForExternalEvent calls with a negative (indefinite) timeout.

  • CreateTimer: TimerOriginCreateTimer on every user-facing timer.
  • waitForExternalEvent: TimerOriginExternalEvent{name} on timeout timers; indefinite timeouts emit a single optional timer with fireAt = sentinel 9999-12-31T23:59:59.999999999Z so the backend has a record of the wait.
  • Activity retry delays: TimerOriginActivityRetry carrying a stable taskExecutionId shared with the ScheduleTaskAction.
  • Child-workflow retry delays: TimerOriginChildWorkflowRetry carrying the first child's instanceId.

Add dropOptionalExternalEventTimerAt(atId) and wire it into handleTaskScheduled, handleSubOrchestrationCreated, and handleTimerCreated so pre-upgrade histories (which lack the synthetic timer emitted by indefinite waitForExternalEvent calls) replay without non-determinism errors. Mirrors the shape of durabletask-go's task/orchestrator.go.

Includes 13 unit tests covering origin assignment across all four call sites and the optional-timer replay-compatibility machinery.

Issue reference

We strive to have all PR being opened based on an issue, where the problem or feature have been discussed prior to implementation.

Please reference the issue this PR will close: #1729

Checklist

Please make sure you've completed the relevant tasks for this PR, out of the following list:

  • Code compiles correctly
  • Created/updated tests
  • Extended the documentation

…tional timers

Annotate every CreateTimerAction the SDK emits with the new origin oneof
from durabletask-protobuf (CreateTimer / ExternalEvent / ActivityRetry /
ChildWorkflowRetry), and emit a synthetic "optional" timer for
waitForExternalEvent calls with a negative (indefinite) timeout.

- CreateTimer: TimerOriginCreateTimer on every user-facing timer.
- waitForExternalEvent: TimerOriginExternalEvent{name} on timeout timers;
  indefinite timeouts emit a single optional timer with fireAt = sentinel
  9999-12-31T23:59:59.999999999Z so the backend has a record of the wait.
- Activity retry delays: TimerOriginActivityRetry carrying a stable
  taskExecutionId shared with the ScheduleTaskAction.
- Child-workflow retry delays: TimerOriginChildWorkflowRetry carrying
  the first child's instanceId.

Add dropOptionalExternalEventTimerAt(atId) and wire it into
handleTaskScheduled, handleSubOrchestrationCreated, and handleTimerCreated
so pre-upgrade histories (which lack the synthetic timer emitted by
indefinite waitForExternalEvent calls) replay without non-determinism
errors. Mirrors the shape of durabletask-go's task/orchestrator.go.

Includes 13 unit tests covering origin assignment across all four call
sites and the optional-timer replay-compatibility machinery.

Fixes dapr#1729

Signed-off-by: Javier Aliaga <javier@diagrid.io>

Copilot AI left a comment

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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Implements timer-origin annotation across all SDK-emitted timers and adds a backwards-compatible “optional” (synthetic) timer for indefinite waitForExternalEvent calls to improve backend observability and replay compatibility (fixes #1729).

Changes:

  • Annotate CreateTimerAction with the new protobuf origin oneof for user timers, external-event timeouts, activity retries, and child-workflow retries.
  • Emit a synthetic sentinel timer for indefinite external-event waits and add replay-compat “drop-and-shift” logic for pre-upgrade histories.
  • Add a comprehensive JUnit test suite covering origin assignment and replay compatibility scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationExecutor.java Adds origin setters for timers, synthetic optional external-event timer emission, and replay-compat drop/shift handling.
durabletask-client/src/test/java/io/dapr/durabletask/TimerOriginTest.java Adds unit tests validating origin assignment and optional-timer replay compatibility behavior.
Comments suppressed due to low confidence (1)

durabletask-client/src/test/java/io/dapr/durabletask/TimerOriginTest.java:1

  • Several pre-patch replay tests hardcode taskExecutionId values (e.g., exec-A, exec-B) even though those IDs are not the focus of the test and may become validated more strictly in the future. To make these tests more robust, consider omitting the field (pass null to taskScheduled) or discover the emitted taskExecutionId in a phase-1 run (as done in Tests 3/4) when the ID matters.
/*

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread durabletask-client/src/test/java/io/dapr/durabletask/TimerOriginTest.java Outdated
… drop unused taskExecutionId in tests

- Extract isSentinelFireAt(Timestamp) helper; use Timestamp.equals()
  instead of manual seconds/nanos comparison in both recognition
  predicates. Proto-generated equals() compares fields exactly, so
  nanosecond fidelity is preserved.
- Tests 10 and 13 exercise drop-and-shift replay-compat and do not
  verify taskExecutionId; pass null instead of hardcoded "exec-A" /
  "exec-B" filler values so the tests stay robust if the SDK ever adds
  stricter validation on that field.

Signed-off-by: Javier Aliaga <javier@diagrid.io>
@javier-aliaga javier-aliaga marked this pull request as ready for review April 24, 2026 15:01
@javier-aliaga javier-aliaga requested review from a team as code owners April 24, 2026 15:01

@siri-varma siri-varma left a comment

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.

LGTM

@dapr-bot dapr-bot merged commit 4396215 into dapr:master Apr 25, 2026
12 checks passed
@codecov

codecov Bot commented Apr 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.76%. Comparing base (4ec9b89) to head (f91fb27).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##             master    #1733   +/-   ##
=========================================
  Coverage     76.76%   76.76%           
  Complexity     2260     2260           
=========================================
  Files           241      241           
  Lines          7066     7066           
  Branches        740      740           
=========================================
  Hits           5424     5424           
  Misses         1279     1279           
  Partials        363      363           

☔ 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.

@nelson-parente nelson-parente added this to the v1.18 milestone Apr 28, 2026
@WhitWaldo WhitWaldo mentioned this pull request May 5, 2026
siri-varma pushed a commit to siri-varma/java-sdk that referenced this pull request May 10, 2026
…tional timers (dapr#1733)

* feat(durabletask): implement timer origin and backwards-compatible optional timers

Annotate every CreateTimerAction the SDK emits with the new origin oneof
from durabletask-protobuf (CreateTimer / ExternalEvent / ActivityRetry /
ChildWorkflowRetry), and emit a synthetic "optional" timer for
waitForExternalEvent calls with a negative (indefinite) timeout.

- CreateTimer: TimerOriginCreateTimer on every user-facing timer.
- waitForExternalEvent: TimerOriginExternalEvent{name} on timeout timers;
  indefinite timeouts emit a single optional timer with fireAt = sentinel
  9999-12-31T23:59:59.999999999Z so the backend has a record of the wait.
- Activity retry delays: TimerOriginActivityRetry carrying a stable
  taskExecutionId shared with the ScheduleTaskAction.
- Child-workflow retry delays: TimerOriginChildWorkflowRetry carrying
  the first child's instanceId.

Add dropOptionalExternalEventTimerAt(atId) and wire it into
handleTaskScheduled, handleSubOrchestrationCreated, and handleTimerCreated
so pre-upgrade histories (which lack the synthetic timer emitted by
indefinite waitForExternalEvent calls) replay without non-determinism
errors. Mirrors the shape of durabletask-go's task/orchestrator.go.

Includes 13 unit tests covering origin assignment across all four call
sites and the optional-timer replay-compatibility machinery.

Fixes dapr#1729

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* refactor(durabletask): address PR feedback — extract sentinel helper, drop unused taskExecutionId in tests

- Extract isSentinelFireAt(Timestamp) helper; use Timestamp.equals()
  instead of manual seconds/nanos comparison in both recognition
  predicates. Proto-generated equals() compares fields exactly, so
  nanosecond fidelity is preserved.
- Tests 10 and 13 exercise drop-and-shift replay-compat and do not
  verify taskExecutionId; pass null instead of hardcoded "exec-A" /
  "exec-B" filler values so the tests stay robust if the SDK ever adds
  stricter validation on that field.

Signed-off-by: Javier Aliaga <javier@diagrid.io>

---------

Signed-off-by: Javier Aliaga <javier@diagrid.io>
siri-varma pushed a commit to siri-varma/java-sdk that referenced this pull request May 10, 2026
…tional timers (dapr#1733)

* feat(durabletask): implement timer origin and backwards-compatible optional timers

Annotate every CreateTimerAction the SDK emits with the new origin oneof
from durabletask-protobuf (CreateTimer / ExternalEvent / ActivityRetry /
ChildWorkflowRetry), and emit a synthetic "optional" timer for
waitForExternalEvent calls with a negative (indefinite) timeout.

- CreateTimer: TimerOriginCreateTimer on every user-facing timer.
- waitForExternalEvent: TimerOriginExternalEvent{name} on timeout timers;
  indefinite timeouts emit a single optional timer with fireAt = sentinel
  9999-12-31T23:59:59.999999999Z so the backend has a record of the wait.
- Activity retry delays: TimerOriginActivityRetry carrying a stable
  taskExecutionId shared with the ScheduleTaskAction.
- Child-workflow retry delays: TimerOriginChildWorkflowRetry carrying
  the first child's instanceId.

Add dropOptionalExternalEventTimerAt(atId) and wire it into
handleTaskScheduled, handleSubOrchestrationCreated, and handleTimerCreated
so pre-upgrade histories (which lack the synthetic timer emitted by
indefinite waitForExternalEvent calls) replay without non-determinism
errors. Mirrors the shape of durabletask-go's task/orchestrator.go.

Includes 13 unit tests covering origin assignment across all four call
sites and the optional-timer replay-compatibility machinery.

Fixes dapr#1729

Signed-off-by: Javier Aliaga <javier@diagrid.io>

* refactor(durabletask): address PR feedback — extract sentinel helper, drop unused taskExecutionId in tests

- Extract isSentinelFireAt(Timestamp) helper; use Timestamp.equals()
  instead of manual seconds/nanos comparison in both recognition
  predicates. Proto-generated equals() compares fields exactly, so
  nanosecond fidelity is preserved.
- Tests 10 and 13 exercise drop-and-shift replay-compat and do not
  verify taskExecutionId; pass null instead of hardcoded "exec-A" /
  "exec-B" filler values so the tests stay robust if the SDK ever adds
  stricter validation on that field.

Signed-off-by: Javier Aliaga <javier@diagrid.io>

---------

Signed-off-by: Javier Aliaga <javier@diagrid.io>
Signed-off-by: Siri Varma Vegiraju <s_vegiraju@apple.com>
javier-aliaga added a commit to javier-aliaga/java-sdk that referenced this pull request Jun 12, 2026
… completion

When an orchestrator returns without calling ctx.complete(), the executor
auto-completes only if no pending actions remain. Since dapr#1733 every
waitForExternalEvent emits a CreateTimer action, so when the final event
arrives in the same work item as the completion that resumed the
orchestrator, the re-armed wait's timer is still pending when the function
returns — blocking the implicit completion. The workflow stayed RUNNING
until that timer fired: forever, for indefinite waits.

Treat pending timers of already-resolved event waits as moot in the
implicit-completion check. All other pending actions block completion as
before.

The regression test reconstructs the failing production history
field-for-field.

Signed-off-by: Javier Aliaga <javier@diagrid.io>
javier-aliaga added a commit to javier-aliaga/java-sdk that referenced this pull request Jun 12, 2026
… completion

When an orchestrator returns without calling ctx.complete(), the executor
auto-completes only if no pending actions remain. Since dapr#1733 every
waitForExternalEvent emits a CreateTimer action, so when the final event
arrives in the same work item as the completion that resumed the
orchestrator, the re-armed wait's timer is still pending when the function
returns — blocking the implicit completion. The workflow stayed RUNNING
until that timer fired: forever, for indefinite waits.

Treat pending timers of already-resolved event waits as obsolete in the
implicit-completion check. All other pending actions block completion as
before.

The regression test reconstructs the failing production history
field-for-field.

Signed-off-by: Javier Aliaga <javier@diagrid.io>
javier-aliaga added a commit that referenced this pull request Jun 15, 2026
… completion (#1769)

When an orchestrator returns without calling ctx.complete(), the executor
auto-completes only if no pending actions remain. Since #1733 every
waitForExternalEvent emits a CreateTimer action, so when the final event
arrives in the same work item as the completion that resumed the
orchestrator, the re-armed wait's timer is still pending when the function
returns — blocking the implicit completion. The workflow stayed RUNNING
until that timer fired: forever, for indefinite waits.

Treat pending timers of already-resolved event waits as obsolete in the
implicit-completion check. All other pending actions block completion as
before.

The regression test reconstructs the failing production history
field-for-field.

Signed-off-by: Javier Aliaga <javier@diagrid.io>
siri-varma pushed a commit that referenced this pull request Jun 16, 2026
… completion (#1769) (#1771)

When an orchestrator returns without calling ctx.complete(), the executor
auto-completes only if no pending actions remain. Since #1733 every
waitForExternalEvent emits a CreateTimer action, so when the final event
arrives in the same work item as the completion that resumed the
orchestrator, the re-armed wait's timer is still pending when the function
returns — blocking the implicit completion. The workflow stayed RUNNING
until that timer fired: forever, for indefinite waits.

Treat pending timers of already-resolved event waits as obsolete in the
implicit-completion check. All other pending actions block completion as
before.

The regression test reconstructs the failing production history
field-for-field.


(cherry picked from commit 625d0f8)

Signed-off-by: Javier Aliaga <javier@diagrid.io>
Co-authored-by: Javier Aliaga <javier@diagrid.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement timer origin

5 participants