Skip to content

Preserve pending durable timers across signal wakeups #96

Description

@rmcdaniel

Observed defect

Published Python SDK 2.4.0 reissues an already scheduled durable timer when a signal wakes the workflow. The second timer receives another durable operation sequence and a later deadline. Once both timers fire, the unchanged workflow can no longer replay its following condition wait. A query reports NonDeterministicReplayError because history contains another timer where the workflow expects the condition.

This reproduces on published Server 2.5.5 before any image replacement. The resulting history also fails replay on published Server 2.5.6, with the same installed SDK and workflow code.

Reproduce

Run a normal Python worker with an echo activity and this workflow:

@workflow.defn(name="pending-timer")
class PendingTimer:
    def __init__(self):
        self.total = 0
        self.released = False

    @workflow.signal("increment")
    def increment(self, amount):
        self.total += amount

    @workflow.signal("release")
    def release(self):
        self.released = True

    @workflow.query("total")
    def current(self):
        return self.total

    def run(self, ctx):
        yield ctx.schedule_activity("echo", ["sample"])
        yield ctx.sleep(120)
        yield ctx.wait_condition(lambda: self.released, key="release")
        return self.total

Start the workflow and wait for ActivityCompleted and TimerScheduled. Send increment(7) while the timer is pending. The query returns seven, but raw Server history now contains two timers for the single sleep call. The observed first timer was operation sequence 2. The second was sequence 3, with the same 120-second delay and a deadline 4.68 seconds later. Wait for the original timers to fire, then query or replay from a fresh worker.

The rejection says workflow sequence 3 recorded TimerScheduled and TimerFired, while the current workflow yielded a condition wait. No application code changed. PHP and Rust controls with the same activity/timer sequence each retain one timer.

Required outcome

  • A wakeup preserves the pending timer's original identity and deadline. Repeated signals and cold worker replacement must not schedule another timer.
  • After the original timer fires, the unchanged workflow reaches its condition wait, remains queryable and completes after release.
  • Check already scheduled remote activities and children for the same resubmission problem. Do not duplicate their durable operations or side effects.
  • Keep genuine changed-code/command mismatch diagnostics. Do not hide damaged histories by skipping arbitrary timer rows.
  • Add focused regression coverage, qualify the published correction against real Server histories and document how already affected histories can be diagnosed and handled.

Found through published cross-language image-continuity qualification. That qualification remains incomplete until this defect is corrected and the workload is rerun.

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

    authority:githubGitHub is the authoritative lifecycle record for this workcompletion:evidence-requiredClose only after all explicit acceptance and operational evidence is publicintake:approvedCurrent issue title and body revision is approved for authority intakekind:defectA public product behavior is incorrectpriority:P1High-priority product or release riskstatus:doneDerived from the authoritative closed issue state

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions