durable-workflow 2.4.0rc1__tar.gz → 2.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {durable_workflow-2.4.0rc1/src/durable_workflow.egg-info → durable_workflow-2.4.1}/PKG-INFO +1 -1
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/pyproject.toml +4 -4
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/workflow.py +13 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_child_workflow_policies.py +3 -4
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_committed_cancellation_scope_descendants.py +2 -1
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_committed_cancellation_scope_groups.py +2 -1
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_committed_cancellation_scope_replay.py +4 -1
- durable_workflow-2.4.1/tests/test_pending_operation_replay.py +154 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_replay_regression_corpus.py +2 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_update_signal_condition_replay.py +10 -7
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/LICENSE +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/README.md +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/setup.cfg +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/worker.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_client.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_errors.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_invocable.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_metrics.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_queries.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_replay.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_schedules.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_serializer.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_signals.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_sleep.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_sync.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_updates.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_worker.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_workflow_updates_conformance.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "2.4.
|
|
7
|
+
version = "2.4.1"
|
|
8
8
|
description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -71,9 +71,9 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
71
71
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
72
72
|
|
|
73
73
|
[tool.durable-workflow]
|
|
74
|
-
product-train = "2.4.
|
|
75
|
-
registry-version = "2.4.
|
|
76
|
-
supported-server-versions = "2.5.0
|
|
74
|
+
product-train = "2.4.1"
|
|
75
|
+
registry-version = "2.4.1"
|
|
76
|
+
supported-server-versions = "2.5.0"
|
|
77
77
|
worker-protocol-version = "1.19"
|
|
78
78
|
control-plane-version = "2"
|
|
79
79
|
durable-selection = true
|
|
@@ -3624,6 +3624,8 @@ def _activity_type_from_payload(payload: Mapping[str, Any]) -> str | None:
|
|
|
3624
3624
|
|
|
3625
3625
|
def _recorded_step_details(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
3626
3626
|
details: dict[str, Any] = {}
|
|
3627
|
+
if "delay_seconds" in payload:
|
|
3628
|
+
details["delay_seconds"] = payload["delay_seconds"]
|
|
3627
3629
|
if "cancellation_cleanup" in payload:
|
|
3628
3630
|
details["cancellation_cleanup"] = payload["cancellation_cleanup"]
|
|
3629
3631
|
local_preparation = payload.get("local_preparation")
|
|
@@ -3846,6 +3848,12 @@ def _recorded_detail_mismatch(command: Any, step: _RecordedStep) -> str | None:
|
|
|
3846
3848
|
f"child_workflow_policy_changed: recorded {field} {recorded_policy!r}, "
|
|
3847
3849
|
f"but current workflow requested {actual_policy!r}."
|
|
3848
3850
|
)
|
|
3851
|
+
elif isinstance(command, StartTimer):
|
|
3852
|
+
if "delay_seconds" in step.details and step.details["delay_seconds"] != command.delay_seconds:
|
|
3853
|
+
return (
|
|
3854
|
+
f"Recorded delay_seconds {step.details['delay_seconds']!r}, but current workflow "
|
|
3855
|
+
f"requested {command.delay_seconds!r}."
|
|
3856
|
+
)
|
|
3849
3857
|
elif isinstance(command, RecordVersionMarker):
|
|
3850
3858
|
recorded = step.details.get("change_id")
|
|
3851
3859
|
if isinstance(recorded, str) and recorded != command.change_id:
|
|
@@ -6667,6 +6675,11 @@ def _replay_state(
|
|
|
6667
6675
|
continue
|
|
6668
6676
|
ctx.logger._set_replaying(False)
|
|
6669
6677
|
_assert_pending_step_matches(cmd)
|
|
6678
|
+
if not isinstance(cmd, RecordLocalActivity) and _next_unconsumed_recorded_step() is not None:
|
|
6679
|
+
# A signal may wake a workflow while its existing operation is
|
|
6680
|
+
# still pending. Reissuing it allocates a new durable sequence
|
|
6681
|
+
# and can move a timer's deadline or duplicate external work.
|
|
6682
|
+
return _state(pending)
|
|
6670
6683
|
if isinstance(cmd, RecordLocalActivity) and prepare_local_activities:
|
|
6671
6684
|
return _state(pending, _prepared_call(cmd, current_call_sequence))
|
|
6672
6685
|
if isinstance(cmd, RecordLocalActivity) and local_activity_executor is not None:
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow.egg-info/SOURCES.txt
RENAMED
|
@@ -81,6 +81,7 @@ tests/test_metrics.py
|
|
|
81
81
|
tests/test_order_processing_example.py
|
|
82
82
|
tests/test_parallel_completion_order_replay.py
|
|
83
83
|
tests/test_parallel_saga_authoring.py
|
|
84
|
+
tests/test_pending_operation_replay.py
|
|
84
85
|
tests/test_prepared_local_activity_client.py
|
|
85
86
|
tests/test_prepared_local_activity_groups.py
|
|
86
87
|
tests/test_prepared_local_activity_policies.py
|
|
@@ -104,9 +104,8 @@ def test_existing_positional_constructor_and_omitted_wire_fields_are_preserved()
|
|
|
104
104
|
result = replay(workflow("sequential", {
|
|
105
105
|
"parent_close_policy": ParentClosePolicy.ABANDON, "cancellation_policy": CancellationPolicy.ABANDON,
|
|
106
106
|
}), scheduled([StartChildWorkflow("child", ["argument"])]), [])
|
|
107
|
-
|
|
108
|
-
assert
|
|
109
|
-
assert result.commands[0].cancellation_policy == CancellationPolicy.ABANDON
|
|
107
|
+
# Omitted historical fields retain their defaults, without starting another child.
|
|
108
|
+
assert result.commands == []
|
|
110
109
|
|
|
111
110
|
|
|
112
111
|
@pytest.mark.parametrize("mode", ["sequential", "parallel", "selection"])
|
|
@@ -120,7 +119,7 @@ def test_changed_policies_fail_cold_replay_and_cancellation_delivery(mode: str,
|
|
|
120
119
|
assert replay(workflow(mode, policies), history, [], run_id="run-1").commands == [CompleteWorkflow("request-1")]
|
|
121
120
|
else:
|
|
122
121
|
cold = replay(workflow(mode, policies), history, [])
|
|
123
|
-
if mode
|
|
122
|
+
if mode in ("sequential", "selection"):
|
|
124
123
|
assert cold.commands == []
|
|
125
124
|
else:
|
|
126
125
|
assert scheduled(cold.commands) == history
|
|
@@ -227,7 +227,8 @@ def test_cleanup_timer_retains_original_ancestor_receipt_and_each_scope_authorit
|
|
|
227
227
|
"2026-10-04T00:00:10.123456Z",
|
|
228
228
|
)
|
|
229
229
|
value["task"].update({"lease_owner": "replacement", "workflow_task_attempt": 17})
|
|
230
|
-
assert
|
|
230
|
+
assert run(cls, value).commands == []
|
|
231
|
+
assert run(cls, value).commands == []
|
|
231
232
|
append_cleanup_event(
|
|
232
233
|
value,
|
|
233
234
|
"TimerFired",
|
|
@@ -217,7 +217,8 @@ def test_group_cleanup_timer_replays_original_receipt_and_narrower_authority(lay
|
|
|
217
217
|
"cancellation_cleanup": snapshot,
|
|
218
218
|
}, "2026-10-04T00:00:09.123456Z")
|
|
219
219
|
value["task"].update({"workflow_task_attempt": 23, "lease_owner": "replacement"})
|
|
220
|
-
assert
|
|
220
|
+
assert run(cls, value).commands == []
|
|
221
|
+
assert run(cls, value).commands == []
|
|
221
222
|
append_cleanup_event(value, "TimerFired", {
|
|
222
223
|
"sequence": 8, "timer_id": "cleanup-timer", "delay_seconds": 1,
|
|
223
224
|
"cancellation_scope_id": original.scope_id,
|
|
@@ -76,7 +76,10 @@ def test_scoped_cleanup_timer_carries_original_delivery_and_replays_without_exte
|
|
|
76
76
|
field: snapshot[field] for field in ("scope_id", "request_id", "delivery_history_event_id")
|
|
77
77
|
}
|
|
78
78
|
assert wire[0]["cancellation_scope_id"] == snapshot["scope_id"]
|
|
79
|
-
|
|
79
|
+
# The original cleanup timer and its authority are already durable.
|
|
80
|
+
# Cold replay waits for it rather than starting a second timer.
|
|
81
|
+
assert run(Probe, value).commands == []
|
|
82
|
+
assert run(Probe, value).commands == []
|
|
80
83
|
append_cleanup_event(value, "TimerFired", {
|
|
81
84
|
"sequence": 4, "timer_id": "cleanup-timer", "delay_seconds": 1,
|
|
82
85
|
"cancellation_scope_id": snapshot["scope_id"],
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Scheduled scalar work stays the same operation when signals wake replay."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from durable_workflow import serializer, workflow
|
|
9
|
+
from durable_workflow.errors import NonDeterministicReplayError
|
|
10
|
+
from durable_workflow.workflow import CompleteWorkflow, WorkflowContext, query_state, replay
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@workflow.defn(name="tests.replay.pending-operation")
|
|
14
|
+
class PendingOperation:
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
self.total = 0
|
|
17
|
+
self.released = False
|
|
18
|
+
|
|
19
|
+
@workflow.signal("increment")
|
|
20
|
+
def increment(self, amount: int) -> None:
|
|
21
|
+
self.total += amount
|
|
22
|
+
|
|
23
|
+
@workflow.signal("release")
|
|
24
|
+
def release(self) -> None:
|
|
25
|
+
self.released = True
|
|
26
|
+
|
|
27
|
+
@workflow.query("total")
|
|
28
|
+
def current(self) -> int:
|
|
29
|
+
return self.total
|
|
30
|
+
|
|
31
|
+
def run(self, ctx: WorkflowContext, kind: str): # type: ignore[no-untyped-def]
|
|
32
|
+
first = yield ctx.schedule_activity("echo", ["sample"])
|
|
33
|
+
if kind == "timer":
|
|
34
|
+
yield ctx.sleep(120)
|
|
35
|
+
elif kind == "activity":
|
|
36
|
+
yield ctx.schedule_activity("work", [first])
|
|
37
|
+
else:
|
|
38
|
+
yield ctx.start_child_workflow("child", [first])
|
|
39
|
+
yield ctx.wait_condition(lambda: self.released, key="release")
|
|
40
|
+
return self.total
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def initial_history() -> list[dict[str, Any]]:
|
|
44
|
+
return [
|
|
45
|
+
{"event_type": "ActivityCompleted", "payload": {
|
|
46
|
+
"sequence": 1, "activity_type": "echo", "result": serializer.envelope("sample"),
|
|
47
|
+
}},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
OPENINGS = {
|
|
52
|
+
"timer": {"event_type": "TimerScheduled", "payload": {
|
|
53
|
+
"sequence": 2, "timer_id": "original-timer", "delay_seconds": 120,
|
|
54
|
+
"fire_at": "2026-10-07T06:02:00Z",
|
|
55
|
+
}},
|
|
56
|
+
"activity": {"event_type": "ActivityScheduled", "payload": {
|
|
57
|
+
"sequence": 2, "activity_type": "work", "activity_execution_id": "original-activity",
|
|
58
|
+
}},
|
|
59
|
+
"child": {"event_type": "ChildWorkflowScheduled", "payload": {
|
|
60
|
+
"sequence": 2, "workflow_type": "child", "child_workflow_instance_id": "original-child",
|
|
61
|
+
}},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize("kind", OPENINGS)
|
|
66
|
+
def test_signal_replay_does_not_reschedule_pending_operation(kind: str) -> None:
|
|
67
|
+
history = initial_history() + [OPENINGS[kind]]
|
|
68
|
+
for amount in (7, 11):
|
|
69
|
+
history.append({"event_type": "SignalReceived", "payload": {
|
|
70
|
+
"signal_name": "increment", "arguments": serializer.envelope([amount]),
|
|
71
|
+
}})
|
|
72
|
+
# Each call constructs a fresh workflow instance, as a replacement worker does.
|
|
73
|
+
outcome = replay(PendingOperation, history, [kind])
|
|
74
|
+
assert outcome.commands == []
|
|
75
|
+
assert query_state(PendingOperation, history, [kind], "total") == sum(
|
|
76
|
+
serializer.decode_envelope(event["payload"]["arguments"])[0]
|
|
77
|
+
for event in history if event["event_type"] == "SignalReceived"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize("kind", OPENINGS)
|
|
82
|
+
def test_new_operation_is_still_scheduled(kind: str) -> None:
|
|
83
|
+
outcome = replay(PendingOperation, initial_history(), [kind])
|
|
84
|
+
assert len(outcome.commands) == 1
|
|
85
|
+
assert outcome.commands[0].to_server_command("queue")["type"] == {
|
|
86
|
+
"timer": "start_timer", "activity": "schedule_activity", "child": "start_child_workflow",
|
|
87
|
+
}[kind]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_original_timer_completion_consumes_signals_once() -> None:
|
|
91
|
+
history = initial_history() + [OPENINGS["timer"],
|
|
92
|
+
{"event_type": "SignalReceived", "payload": {
|
|
93
|
+
"signal_name": "increment", "arguments": serializer.envelope([7]),
|
|
94
|
+
}},
|
|
95
|
+
{"event_type": "TimerFired", "payload": {"sequence": 2, "timer_id": "original-timer"}},
|
|
96
|
+
{"event_type": "SignalReceived", "payload": {
|
|
97
|
+
"signal_name": "release", "arguments": serializer.envelope([]),
|
|
98
|
+
}},
|
|
99
|
+
]
|
|
100
|
+
outcome = replay(PendingOperation, history, ["timer"])
|
|
101
|
+
assert len(outcome.commands) == 1
|
|
102
|
+
assert isinstance(outcome.commands[0], CompleteWorkflow)
|
|
103
|
+
assert outcome.commands[0].result == 7
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_pending_timer_still_rejects_changed_delay() -> None:
|
|
107
|
+
changed = {"event_type": "TimerScheduled", "payload": {**OPENINGS["timer"]["payload"], "delay_seconds": 121}}
|
|
108
|
+
with pytest.raises(NonDeterministicReplayError) as captured:
|
|
109
|
+
replay(PendingOperation, initial_history() + [changed], ["timer"])
|
|
110
|
+
assert captured.value.workflow_sequence == 2
|
|
111
|
+
assert captured.value.recorded_event_types == ["TimerScheduled"]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.mark.parametrize("kind", ["activity", "child"])
|
|
115
|
+
def test_original_external_operation_completion_consumes_signals_once(kind: str) -> None:
|
|
116
|
+
terminal = {
|
|
117
|
+
"event_type": "ActivityCompleted" if kind == "activity" else "ChildRunCompleted",
|
|
118
|
+
"payload": {**OPENINGS[kind]["payload"], "result": serializer.envelope("original-result")},
|
|
119
|
+
}
|
|
120
|
+
history = initial_history() + [OPENINGS[kind],
|
|
121
|
+
{"event_type": "SignalReceived", "payload": {
|
|
122
|
+
"signal_name": "increment", "arguments": serializer.envelope([7]),
|
|
123
|
+
}}, terminal,
|
|
124
|
+
{"event_type": "SignalReceived", "payload": {
|
|
125
|
+
"signal_name": "release", "arguments": serializer.envelope([]),
|
|
126
|
+
}},
|
|
127
|
+
]
|
|
128
|
+
assert replay(PendingOperation, history, [kind]).commands == [CompleteWorkflow(7)]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.parametrize("kind", OPENINGS)
|
|
132
|
+
def test_recorded_operation_cannot_be_replaced_by_a_different_kind(kind: str) -> None:
|
|
133
|
+
different = "activity" if kind == "timer" else "timer"
|
|
134
|
+
with pytest.raises(NonDeterministicReplayError) as captured:
|
|
135
|
+
replay(PendingOperation, initial_history() + [OPENINGS[kind]], [different])
|
|
136
|
+
assert captured.value.workflow_sequence == 2
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_legacy_timer_without_recorded_delay_still_waits_for_original_operation() -> None:
|
|
140
|
+
legacy = {"event_type": "TimerScheduled", "payload": {"sequence": 2, "timer_id": "original-timer"}}
|
|
141
|
+
assert replay(PendingOperation, initial_history() + [legacy], ["timer"]).commands == []
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_already_duplicated_timers_are_not_silently_skipped() -> None:
|
|
145
|
+
duplicate = {"event_type": "TimerScheduled", "payload": {
|
|
146
|
+
**OPENINGS["timer"]["payload"], "sequence": 3, "timer_id": "duplicate-timer",
|
|
147
|
+
}}
|
|
148
|
+
history = initial_history() + [OPENINGS["timer"], duplicate,
|
|
149
|
+
{"event_type": "TimerFired", "payload": {"sequence": 2, "timer_id": "original-timer"}},
|
|
150
|
+
{"event_type": "TimerFired", "payload": {"sequence": 3, "timer_id": "duplicate-timer"}},
|
|
151
|
+
]
|
|
152
|
+
with pytest.raises(NonDeterministicReplayError) as captured:
|
|
153
|
+
replay(PendingOperation, history, ["timer"])
|
|
154
|
+
assert captured.value.workflow_sequence == 3
|
|
@@ -24,6 +24,7 @@ from tests.test_golden_history_replay import (
|
|
|
24
24
|
GoldenTimeoutWaitWorkflow,
|
|
25
25
|
GoldenVersionMarkerWorkflow,
|
|
26
26
|
)
|
|
27
|
+
from tests.test_pending_operation_replay import PendingOperation
|
|
27
28
|
from tests.test_update_signal_condition_replay import (
|
|
28
29
|
PostConditionReceiversWorkflow,
|
|
29
30
|
UpdateSignalConditionTimerWorkflow,
|
|
@@ -265,6 +266,7 @@ WORKFLOWS = [
|
|
|
265
266
|
NestedParallelPathWorkflow,
|
|
266
267
|
ParallelMetadataProducerWorkflow,
|
|
267
268
|
ParallelResultBindingWorkflow,
|
|
269
|
+
PendingOperation,
|
|
268
270
|
PagedRecordedSideEffectsWorkflow,
|
|
269
271
|
PostConditionReceiversWorkflow,
|
|
270
272
|
RecordedSideEffectWorkflow,
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_update_signal_condition_replay.py
RENAMED
|
@@ -460,13 +460,13 @@ class TestUpdateSignalConditionReplay:
|
|
|
460
460
|
expected_commands = [
|
|
461
461
|
StartTimer,
|
|
462
462
|
StartTimer,
|
|
463
|
-
|
|
464
|
-
ScheduleActivity,
|
|
465
|
-
ScheduleActivity,
|
|
466
|
-
ScheduleActivity,
|
|
467
|
-
ScheduleActivity,
|
|
463
|
+
None,
|
|
468
464
|
ScheduleActivity,
|
|
465
|
+
None,
|
|
466
|
+
None,
|
|
469
467
|
ScheduleActivity,
|
|
468
|
+
None,
|
|
469
|
+
None,
|
|
470
470
|
WaitCondition,
|
|
471
471
|
WaitCondition,
|
|
472
472
|
WaitCondition,
|
|
@@ -475,8 +475,8 @@ class TestUpdateSignalConditionReplay:
|
|
|
475
475
|
WaitCondition,
|
|
476
476
|
ScheduleActivity,
|
|
477
477
|
ScheduleActivity,
|
|
478
|
-
|
|
479
|
-
|
|
478
|
+
None,
|
|
479
|
+
None,
|
|
480
480
|
WaitCondition,
|
|
481
481
|
WaitCondition,
|
|
482
482
|
WaitCondition,
|
|
@@ -492,6 +492,9 @@ class TestUpdateSignalConditionReplay:
|
|
|
492
492
|
[],
|
|
493
493
|
)
|
|
494
494
|
|
|
495
|
+
if expected_command is None:
|
|
496
|
+
assert outcome.commands == [], prefix_length
|
|
497
|
+
continue
|
|
495
498
|
assert len(outcome.commands) == 1, prefix_length
|
|
496
499
|
assert isinstance(outcome.commands[0], expected_command), prefix_length
|
|
497
500
|
if prefix_length in (15, 16, 17, 18):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/_activity_process.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/auth_composition.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/external_storage.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/history_bundle_verify.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow/replay_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_activity_cancellation_policies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_remaining_time.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_scope_authoring.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_scope_boundary.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cancellation_scope_opening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cooperative_cancellation_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cooperative_cancellation_worker.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_cooperative_remote_worker.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_docstring_cross_references.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_prepared_local_activity_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_prepared_local_activity_policies.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_prepared_local_activity_worker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_scoped_run_cancellation_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_standalone_activity_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.0rc1 → durable_workflow-2.4.1}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|