Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Replay of Workflow 2.19 redrive reuse is qualified for `MemoUpserted` and for `SearchAttributesUpserted` value identity. Neither event is a replay clock source: `ctx.now()` stays the persisted start time, which on a redriven run is the failed run's start, and the cancellation budget does not move on an upsert. Search-attribute type identity is not qualified, because the Server records namespace-registered types the SDK cannot see. Signal waits (`SignalWaitOpened`, `SignalReceived`, `SignalApplied`) are not qualified for Python: the SDK has no signal-wait primitive. Replaying such reused events through a signal handler applies the signal once and takes no time from them, but that does not qualify signal waits. (#235)
- Replay of Workflow 2.19 redrive reuse is qualified for `MemoUpserted` and for `SearchAttributesUpserted` value identity. Neither event is a replay clock source: `ctx.now()` stays the persisted start time, which on a redriven run is the failed run's start, and the cancellation budget does not move on an upsert. Search-attribute type identity is not qualified. The SDK's upsert declares no `attribute_types`, and the Server records namespace-registered types the SDK cannot see, so recorded types are not compared with authored ones. They are only checked to be well formed and able to hold the recorded value. Signal waits (`SignalWaitOpened`, `SignalReceived`, `SignalApplied`) are scoped out for Python, because the SDK cannot author a Core signal wait. Reused signal-wait events replay for history validity only: a cold replay applies the recorded signal once, takes no time from these events and opens no wait again. This does not qualify Python workers for Core signal waits. (#235)

## [2.13.1] - 2026-11-03

Expand Down
24 changes: 23 additions & 1 deletion tests/test_replay_clock.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from durable_workflow import serializer, workflow
from durable_workflow.errors import NonDeterministicReplayError, WorkflowCancelled
from durable_workflow.workflow import CompleteWorkflow, WorkflowContext, query_state, replay
from durable_workflow.workflow import CompleteWorkflow, WaitCondition, WorkflowContext, query_state, replay
from tests.redrive_fixture_registry import VALID_SUCCESSORS


Expand Down Expand Up @@ -344,6 +344,28 @@
assert serializer.decode(received["arguments"], codec="avro") == ["Grace"]


def test_a_cold_worker_replays_reused_signal_wait_events_without_reopening_the_wait() -> None:
# #235 scope: the SDK cannot author a Core signal wait, so these reused
# events replay for history validity only. A cold replay applies the
# recorded signal once and opens no wait again. This does not qualify
# Python workers for Core signal waits: the counterpart's next call is
# numbered from 1, while Core's wait occupies step 1.
history = redriven("redrive-reused-signal-wait-clock")
for _ in range(2):
RedriveSignalWorkflow.applied = []

Check warning on line 355 in tests/test_replay_clock.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "monkeypatch" fixture for temporary modifications instead of manually modifying global state.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sdk-python&issues=AaGkadP_VLx5e8YcYTpH&open=AaGkadP_VLx5e8YcYTpH&pullRequest=243
commands = replay_redriven(RedriveSignalWorkflow, history)
assert not any(isinstance(command, WaitCondition) for command in commands)
assert RedriveSignalWorkflow.applied == ["Grace"]
# Control: without the recorded signal the same replay opens the wait.
unsignalled = [deepcopy(event) for event in history
if event["event_type"] not in ("SignalReceived", "SignalApplied")]
for number, event in enumerate(unsignalled, start=1):
event["sequence"] = number
RedriveSignalWorkflow.applied = []

Check warning on line 364 in tests/test_replay_clock.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the "monkeypatch" fixture for temporary modifications instead of manually modifying global state.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_sdk-python&issues=AaGkadP_VLx5e8YcYTpI&open=AaGkadP_VLx5e8YcYTpI&pullRequest=243
assert [type(command) for command in replay_redriven(RedriveSignalWorkflow, unsignalled)] == [WaitCondition]
assert RedriveSignalWorkflow.applied == []


def _retimed(history: list[dict[str, Any]], event_type: str, mode: str) -> list[dict[str, Any]]:
changed = deepcopy(history)
event = only_event(changed, event_type)
Expand Down
Loading