Skip to content

fix: use deterministic time in createTimer instead of Date.now()#146

Open
YunchuWang wants to merge 2 commits intomainfrom
fix-createTimer-determinism
Open

fix: use deterministic time in createTimer instead of Date.now()#146
YunchuWang wants to merge 2 commits intomainfrom
fix-createTimer-determinism

Conversation

@YunchuWang
Copy link
Member

Summary

Replaces Date.now() with this._currentUtcDatetime.getTime() in RuntimeOrchestrationContext.createTimer() so that the timer fire-at time is computed deterministically from the orchestration time rather than wall clock time.

Problem

createTimer() used Date.now() when converting a relative seconds value to an absolute Date. This is a determinism contract violation — during replay, Date.now() returns the current wall clock time (which differs from the original execution time), producing a different timer fire-at value.

Fix

Use this._currentUtcDatetime.getTime() instead, which returns the deterministic orchestration time set by OrchestratorStarted events. This is consistent with how other Durable Task SDKs handle relative timer delays.

Fixes #134

YunchuWang and others added 2 commits March 5, 2026 16:41
Fixes #131

The WhenAllTask constructor redundantly re-initialized _completedTasks
and _failedTasks to 0 after calling super(tasks). Since CompositeTask's
constructor already initializes these fields and then processes
pre-completed children via onChildCompleted(), the reset wiped out
the correct count, causing WhenAllTask to never complete when some
children were already complete at construction time.

Also removes _failedTasks reset (dead code - never incremented anywhere).

Added 8 unit tests for WhenAllTask covering:
- Empty task array
- All pending children completing
- Fail-fast on child failure
- Pre-completed children (the bug scenario)
- All children pre-completed
- Pre-failed child
- Post-fail-fast completion
- Pending tasks count tracking
Replace Date.now() with this._currentUtcDatetime.getTime() in
RuntimeOrchestrationContext.createTimer() to ensure the timer fire-at
time is computed deterministically from the orchestration time rather
than wall clock time. This fixes a determinism contract violation
during replay.

Fixes #134

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 6, 2026 00:50

import { WhenAllTask } from "../src/task/when-all-task";
import { CompletableTask } from "../src/task/completable-task";
import { Task } from "../src/task/task";
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an orchestrator determinism violation by computing relative timer deadlines from the orchestration’s deterministic time (currentUtcDateTime) rather than wall-clock time, and also includes a small WhenAllTask correctness fix + new unit tests around WhenAllTask behavior.

Changes:

  • Make RuntimeOrchestrationContext.createTimer(number) compute fireAt from this._currentUtcDatetime instead of Date.now() to preserve determinism during replay.
  • Remove WhenAllTask counter re-initialization (documenting why it must not happen after super()).
  • Add a new Jest spec file covering WhenAllTask edge-cases (empty array, pre-completed children, fail-fast, pending count).

Reviewed changes

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

File Description
packages/durabletask-js/src/worker/runtime-orchestration-context.ts Switches relative timer scheduling to use deterministic orchestration time.
packages/durabletask-js/src/task/when-all-task.ts Prevents counter reset after CompositeTask processes pre-completed children.
packages/durabletask-js/test/when-all-task.spec.ts Adds unit tests validating WhenAllTask completion/fail-fast semantics and counters.
Comments suppressed due to low confidence (1)

packages/durabletask-js/src/worker/runtime-orchestration-context.ts:304

  • The determinism fix in createTimer(number) is important but currently not covered by a targeted unit test. Add a test that sets a known currentUtcDateTime (via OrchestratorStarted), calls ctx.createTimer(<seconds>), and asserts the generated timer action’s fireAt equals startTime + seconds, including across replay.
    // If a number is passed, we use it as the number of seconds to wait
    // we use instanceof Date as number is not a native Javascript type
    if (!(fireAt instanceof Date)) {
      fireAt = new Date(this._currentUtcDatetime.getTime() + fireAt * 1000);
    }

    const action = ph.newCreateTimerAction(id, fireAt);
    this._pendingActions[action.getId()] = action;

Comment on lines +45 to +56
// Issue #131: WhenAllTask constructor resets _completedTasks counter
it("should complete correctly when constructed with pre-completed children", () => {
const child1 = new CompletableTask<number>();
const child2 = new CompletableTask<number>();
const child3 = new CompletableTask<number>();

// Complete child1 and child2 before constructing WhenAllTask
child1.complete(10);
child2.complete(20);

const task = new WhenAllTask([child1, child2, child3]);

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

PR title/description focuses on createTimer() determinism (Issue #134), but this PR also changes WhenAllTask behavior/tests (Issue #131). Consider updating the PR description to mention the additional scope or splitting into separate PRs to keep changesets focused.

Copilot uses AI. Check for mistakes.

import { WhenAllTask } from "../src/task/when-all-task";
import { CompletableTask } from "../src/task/completable-task";
import { Task } from "../src/task/task";
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

Unused import Task will fail repo linting (@typescript-eslint/no-unused-vars is configured as an error). Remove the import or use it.

Suggested change
import { Task } from "../src/task/task";

Copilot uses AI. Check for mistakes.
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.

[copilot-finds] Bug: createTimer(seconds) uses Date.now() instead of currentUtcDateTime, violating orchestrator determinism

2 participants