Skip to content

Workflow now() exposes a mutable replay clock and can expire deadlines immediately #511

Description

@rmcdaniel

Observed Defect

In the published Workflow 2.0.13 package (PHP 8.4), Workflow\V2\now() returns the mutable Carbon instance held by WorkflowFiberContext. Deriving a deadline with now()->addHour() mutates the clock itself. A subsequent now() is already at that deadline, despite no history event advancing time.

Minimal reproduction against the installed package:

require 'vendor/autoload.php';

$fiber = new Fiber(function () {
    \Workflow\V2\Support\WorkflowFiberContext::setTime(
        \Carbon\Carbon::parse('2026-01-01T00:00:00Z')
    );
    $before = \Workflow\V2\now()->getTimestamp();
    $deadline = \Workflow\V2\now()->addHour();
    echo json_encode([
        'clock_advanced_seconds' => \Workflow\V2\now()->getTimestamp() - $before,
        'deadline_already_reached' => \Workflow\V2\now()->greaterThanOrEqualTo($deadline),
    ]);
});
$fiber->start();

Observed: {"clock_advanced_seconds":3600,"deadline_already_reached":true}.
Expected: 0 and false.

Also reproduced in a real database-backed workflow before its first activity. Later history replay can replace the stored clock, which makes the symptom depend on where deadline arithmetic occurs.

Fix And Qualification

  • Return an isolated clock value (defensive copy or immutable instance) without exposing mutable executor state.
  • Inspect setTime() ownership too: retaining a caller-owned mutable object can expose the same alias in the opposite direction.
  • Preserve deterministic history timestamps; do not replace workflow time with wall time.
  • Cover two consecutive clock reads, mutation of an earlier returned value, mutation of the supplied clock object, and separate fibers.
  • Prove an ordinary deadline workflow remains pending initially and crosses its deadline only after durable time advancement and replay.
  • Review affected consumers and run focused published-artifact conformance after release.

Immediate application-level mitigation: derive deadlines from now()->toImmutable(). This does not replace the engine fix.

No public API or private infrastructure change is requested.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:defectA public product behavior is incorrectpriority:P1High-priority product or release risk

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions