durable-workflow 2.5.0__tar.gz → 2.5.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.5.0/src/durable_workflow.egg-info → durable_workflow-2.5.1}/PKG-INFO +1 -1
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/pyproject.toml +4 -4
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/workflow.py +94 -12
- {durable_workflow-2.5.0 → durable_workflow-2.5.1/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cooperative_remote_worker.py +10 -3
- durable_workflow-2.5.1/tests/test_patch_history.py +145 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_replay_regression_corpus.py +4 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/LICENSE +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/README.md +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/setup.cfg +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/worker.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_client.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_errors.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_invocable.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_metrics.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_queries.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_replay.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_schedules.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_serializer.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_signals.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_sleep.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_sync.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_updates.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_worker.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.5.0 → durable_workflow-2.5.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.5.
|
|
7
|
+
version = "2.5.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.5.
|
|
75
|
-
registry-version = "2.5.
|
|
76
|
-
supported-server-versions = "2.5.
|
|
74
|
+
product-train = "2.5.1"
|
|
75
|
+
registry-version = "2.5.1"
|
|
76
|
+
supported-server-versions = "2.5.16"
|
|
77
77
|
worker-protocol-version = "1.19"
|
|
78
78
|
control-plane-version = "2"
|
|
79
79
|
durable-selection = true
|
|
@@ -5185,6 +5185,96 @@ def _replay_state(
|
|
|
5185
5185
|
pending: list[Command] = []
|
|
5186
5186
|
advanced_cmd: Any = None
|
|
5187
5187
|
terminal_condition_reopen_cmd: WaitCondition | None = None
|
|
5188
|
+
version_decisions: dict[str, tuple[int, str]] = {}
|
|
5189
|
+
|
|
5190
|
+
def _version_error(detail: str, step: _RecordedStep | None = None) -> NonDeterministicReplayError:
|
|
5191
|
+
return NonDeterministicReplayError(
|
|
5192
|
+
step.workflow_sequence if step is not None else authored_sequence,
|
|
5193
|
+
"compatible version marker",
|
|
5194
|
+
step.event_types if step is not None else [],
|
|
5195
|
+
detail=detail,
|
|
5196
|
+
)
|
|
5197
|
+
|
|
5198
|
+
def _recorded_version(step: _RecordedStep) -> int:
|
|
5199
|
+
if step.history_index is None:
|
|
5200
|
+
raise _version_error("Version marker is missing its history position.", step)
|
|
5201
|
+
payload = events[step.history_index].get("payload") or {}
|
|
5202
|
+
version = payload.get("version")
|
|
5203
|
+
if type(version) is not int:
|
|
5204
|
+
raise _version_error("Version marker must contain an integer version.", step)
|
|
5205
|
+
change_id = payload.get("change_id")
|
|
5206
|
+
if "change_id" in payload and (not isinstance(change_id, str) or not change_id.strip()):
|
|
5207
|
+
raise _version_error("Version marker must retain a non-empty change ID.", step)
|
|
5208
|
+
if "min_supported" in payload or "max_supported" in payload:
|
|
5209
|
+
minimum, maximum = payload.get("min_supported"), payload.get("max_supported")
|
|
5210
|
+
if (type(minimum) is not int or type(maximum) is not int
|
|
5211
|
+
or not minimum <= version <= maximum):
|
|
5212
|
+
raise _version_error("Recorded version marker has an invalid supported range.", step)
|
|
5213
|
+
return version
|
|
5214
|
+
|
|
5215
|
+
def _consume_duplicate_version_markers() -> None:
|
|
5216
|
+
nonlocal result_cursor, authored_sequence
|
|
5217
|
+
if not version_decisions:
|
|
5218
|
+
return
|
|
5219
|
+
while result_cursor < len(recorded_steps):
|
|
5220
|
+
step = recorded_steps[result_cursor]
|
|
5221
|
+
change_id = step.details.get("change_id")
|
|
5222
|
+
if step.shape != "version marker" or change_id not in version_decisions:
|
|
5223
|
+
return
|
|
5224
|
+
if _next_unconsumed_recorded_step() is not step:
|
|
5225
|
+
return
|
|
5226
|
+
version = _recorded_version(step)
|
|
5227
|
+
if version != version_decisions[change_id][0]:
|
|
5228
|
+
raise _version_error(f"Conflicting recorded decisions for change ID {change_id!r}.", step)
|
|
5229
|
+
# Earlier SDKs emitted a marker for every invocation. Consume these
|
|
5230
|
+
# aliases while retaining their physical positions for cancellation
|
|
5231
|
+
# and prepared operations. The stored history itself is unchanged.
|
|
5232
|
+
result_cursor += 1
|
|
5233
|
+
authored_sequence = max(authored_sequence, step.workflow_sequence + 1)
|
|
5234
|
+
|
|
5235
|
+
def _resolve_version(command: RecordVersionMarker) -> Any:
|
|
5236
|
+
nonlocal result_cursor, authored_sequence
|
|
5237
|
+
change_id = command.change_id
|
|
5238
|
+
if not isinstance(change_id, str) or not change_id.strip():
|
|
5239
|
+
raise _version_error("Version markers require a stable non-empty change ID.")
|
|
5240
|
+
if (type(command.min_supported) is not int or type(command.max_supported) is not int
|
|
5241
|
+
or command.min_supported > command.max_supported):
|
|
5242
|
+
raise _version_error(f"Invalid supported range for change ID {change_id!r}.")
|
|
5243
|
+
family = "version" if command.result_kind == "version" else "patch"
|
|
5244
|
+
decision = version_decisions.get(change_id)
|
|
5245
|
+
step = _next_unconsumed_recorded_step()
|
|
5246
|
+
if decision is not None:
|
|
5247
|
+
version, original_family = decision
|
|
5248
|
+
if original_family != family:
|
|
5249
|
+
raise _version_error(f"Change ID {change_id!r} cannot switch between version and patch helpers.", step)
|
|
5250
|
+
elif step is not None and step.shape == "version marker":
|
|
5251
|
+
_assert_step_matches(command, step)
|
|
5252
|
+
version = _recorded_version(step)
|
|
5253
|
+
result_cursor += 1
|
|
5254
|
+
authored_sequence = max(authored_sequence, step.workflow_sequence + 1)
|
|
5255
|
+
elif step is not None or (cancellation.delivery is not None and not cancellation_consumed) or any(
|
|
5256
|
+
sequence not in consumed_scope_deliveries for sequence in committed_scopes.deliveries
|
|
5257
|
+
) or any(_history_event_type(event) in {
|
|
5258
|
+
"WorkflowCompleted", "WorkflowFailed", "WorkflowCancelled", "WorkflowTerminated",
|
|
5259
|
+
"WorkflowContinuedAsNew", "WorkflowTimedOut",
|
|
5260
|
+
} for event in events):
|
|
5261
|
+
# An unmarked recorded command proves this history passed the new
|
|
5262
|
+
# patch site. Leave that command available for its original call.
|
|
5263
|
+
version = -1
|
|
5264
|
+
else:
|
|
5265
|
+
version = command.version
|
|
5266
|
+
if type(version) is not int:
|
|
5267
|
+
raise _version_error(f"Invalid version for change ID {change_id!r}.")
|
|
5268
|
+
ctx.logger._set_replaying(False)
|
|
5269
|
+
pending.append(command)
|
|
5270
|
+
authored_sequence += 1
|
|
5271
|
+
if not command.min_supported <= version <= command.max_supported:
|
|
5272
|
+
raise _version_error(
|
|
5273
|
+
f"Recorded version {version} for change ID {change_id!r} is outside "
|
|
5274
|
+
f"the supported range {command.min_supported}..{command.max_supported}.", step,
|
|
5275
|
+
)
|
|
5276
|
+
version_decisions.setdefault(change_id, (version, family))
|
|
5277
|
+
return _version_marker_result(command, version)
|
|
5188
5278
|
|
|
5189
5279
|
def _parallel_leaf_kind(command: Any) -> str:
|
|
5190
5280
|
if isinstance(command, ScheduleActivity | RecordLocalActivity):
|
|
@@ -6064,6 +6154,7 @@ def _replay_state(
|
|
|
6064
6154
|
replay_token = _ACTIVE_WORKFLOW_REPLAY.set(ctx)
|
|
6065
6155
|
try:
|
|
6066
6156
|
while True:
|
|
6157
|
+
_consume_duplicate_version_markers()
|
|
6067
6158
|
# Cursor-0 receivers are start-boundary events. Enter run() once
|
|
6068
6159
|
# before applying them so workflow initialization observes the
|
|
6069
6160
|
# same WorkflowStarted-before-Signal/Update ordering the server
|
|
@@ -6081,6 +6172,9 @@ def _replay_state(
|
|
|
6081
6172
|
first = False
|
|
6082
6173
|
_apply_due_receivers()
|
|
6083
6174
|
_validate_local_activity_policies(cmd)
|
|
6175
|
+
if isinstance(cmd, RecordVersionMarker):
|
|
6176
|
+
next_value = _resolve_version(cmd)
|
|
6177
|
+
continue
|
|
6084
6178
|
if prepare_local_activities and isinstance(cmd, list | SelectGroup) and _contains_local_activity(cmd) and (
|
|
6085
6179
|
not prepare_local_activity_groups or isinstance(cmd, SelectGroup)
|
|
6086
6180
|
):
|
|
@@ -6497,18 +6591,6 @@ def _replay_state(
|
|
|
6497
6591
|
pending.append(cmd)
|
|
6498
6592
|
next_value = None
|
|
6499
6593
|
continue
|
|
6500
|
-
if isinstance(cmd, RecordVersionMarker):
|
|
6501
|
-
if result_cursor < len(resolved_results):
|
|
6502
|
-
_assert_next_step_matches(cmd)
|
|
6503
|
-
val = resolved_results[result_cursor]
|
|
6504
|
-
result_cursor += 1
|
|
6505
|
-
next_value = _version_marker_result(cmd, val)
|
|
6506
|
-
continue
|
|
6507
|
-
ctx.logger._set_replaying(False)
|
|
6508
|
-
_assert_pending_step_matches(cmd)
|
|
6509
|
-
pending.append(cmd)
|
|
6510
|
-
next_value = _version_marker_result(cmd, cmd.version)
|
|
6511
|
-
continue
|
|
6512
6594
|
if isinstance(cmd, WaitCondition):
|
|
6513
6595
|
# A condition that is already true is not persisted. Apply
|
|
6514
6596
|
# receivers at the yield boundary before deciding whether the
|
|
@@ -82,6 +82,7 @@ tests/test_metrics.py
|
|
|
82
82
|
tests/test_order_processing_example.py
|
|
83
83
|
tests/test_parallel_completion_order_replay.py
|
|
84
84
|
tests/test_parallel_saga_authoring.py
|
|
85
|
+
tests/test_patch_history.py
|
|
85
86
|
tests/test_pending_operation_replay.py
|
|
86
87
|
tests/test_prepared_local_activity_client.py
|
|
87
88
|
tests/test_prepared_local_activity_groups.py
|
|
@@ -47,22 +47,29 @@ def stop_receipt() -> dict[str, Any]:
|
|
|
47
47
|
"heartbeat_recorded": False, "history_event_id": "receipt-event"}
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def write_pid_marker(marker: str) -> None:
|
|
51
|
+
target = Path(marker)
|
|
52
|
+
pending = target.with_name(target.name + ".pending")
|
|
53
|
+
pending.write_text(str(os.getpid()))
|
|
54
|
+
pending.replace(target)
|
|
55
|
+
|
|
56
|
+
|
|
50
57
|
async def blocked_async(marker: str, heartbeat: bool = False) -> None:
|
|
51
58
|
if heartbeat:
|
|
52
59
|
await activity.context().heartbeat({"authored": True})
|
|
53
|
-
|
|
60
|
+
write_pid_marker(marker)
|
|
54
61
|
await asyncio.Event().wait()
|
|
55
62
|
|
|
56
63
|
|
|
57
64
|
def blocked_sync(marker: str, heartbeat: bool = False) -> None:
|
|
58
65
|
if heartbeat:
|
|
59
66
|
asyncio.run(activity.context().heartbeat({"authored": True}))
|
|
60
|
-
|
|
67
|
+
write_pid_marker(marker)
|
|
61
68
|
time.sleep(60)
|
|
62
69
|
|
|
63
70
|
|
|
64
71
|
def released_sync(marker: str) -> bytes:
|
|
65
|
-
|
|
72
|
+
write_pid_marker(marker)
|
|
66
73
|
while not Path(marker + ".release").exists():
|
|
67
74
|
time.sleep(0.02)
|
|
68
75
|
return b"late result"
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from durable_workflow import serializer, workflow
|
|
8
|
+
from durable_workflow.errors import NonDeterministicReplayError, WorkflowCancelled
|
|
9
|
+
from durable_workflow.workflow import CompleteWorkflow, RecordVersionMarker, ScheduleActivity, replay
|
|
10
|
+
from tests.test_cooperative_cancellation import marker as cancellation_marker
|
|
11
|
+
from tests.test_cooperative_cancellation import request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def version_marker(sequence: int, change_id: str = "same", version: int = 1) -> dict[str, Any]:
|
|
15
|
+
return {"event_type": "VersionMarkerRecorded", "payload": {
|
|
16
|
+
"sequence": sequence, "change_id": change_id, "version": version,
|
|
17
|
+
"min_supported": -1, "max_supported": 1,
|
|
18
|
+
}}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def old_activity(sequence: int, completed: bool = True) -> list[dict[str, Any]]:
|
|
22
|
+
history = [{"event_type": "ActivityScheduled", "payload": {
|
|
23
|
+
"sequence": sequence, "activity_type": "old",
|
|
24
|
+
}}]
|
|
25
|
+
if completed:
|
|
26
|
+
history.append({"event_type": "ActivityCompleted", "payload": {
|
|
27
|
+
"sequence": sequence, "activity_type": "old", "result": serializer.encode("recorded"),
|
|
28
|
+
"payload_codec": "avro",
|
|
29
|
+
}})
|
|
30
|
+
return history
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@workflow.defn(name="tests.patch.insertion")
|
|
34
|
+
class PatchInsertionWorkflow:
|
|
35
|
+
def run(self, ctx): # type: ignore[no-untyped-def]
|
|
36
|
+
if (yield ctx.patched("same")):
|
|
37
|
+
yield ctx.schedule_activity("new", [])
|
|
38
|
+
return (yield ctx.schedule_activity("old", []))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@workflow.defn(name="tests.patch.repeated")
|
|
42
|
+
class RepeatedPatchWorkflow:
|
|
43
|
+
def run(self, ctx, repeat=True): # type: ignore[no-untyped-def]
|
|
44
|
+
first = yield ctx.patched("same")
|
|
45
|
+
second = (yield ctx.patched("same")) if repeat else first
|
|
46
|
+
result = yield ctx.schedule_activity("old", [])
|
|
47
|
+
return [first, second, result]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@workflow.defn(name="tests.patch.interleaved")
|
|
51
|
+
class InterleavedPatchWorkflow:
|
|
52
|
+
def run(self, ctx, repeat=True): # type: ignore[no-untyped-def]
|
|
53
|
+
first = yield ctx.patched("same")
|
|
54
|
+
result = yield ctx.schedule_activity("old", [])
|
|
55
|
+
second = (yield ctx.patched("same")) if repeat else first
|
|
56
|
+
return [first, second, result]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@workflow.defn(name="tests.patch.ranges")
|
|
60
|
+
class VersionRangeWorkflow:
|
|
61
|
+
def run(self, ctx, minimum=1, mixed=False): # type: ignore[no-untyped-def]
|
|
62
|
+
first = yield ctx.get_version("same", 1, 2)
|
|
63
|
+
second = (yield ctx.patched("same")) if mixed else (yield ctx.get_version("same", minimum, 3))
|
|
64
|
+
return [first, second]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@workflow.defn(name="tests.patch.cancellation")
|
|
68
|
+
class PatchCancellationWorkflow:
|
|
69
|
+
def run(self, ctx): # type: ignore[no-untyped-def]
|
|
70
|
+
decisions = [(yield ctx.patched("same")), (yield ctx.patched("same"))]
|
|
71
|
+
try:
|
|
72
|
+
yield ctx.schedule_activity("old", [])
|
|
73
|
+
except WorkflowCancelled:
|
|
74
|
+
return [decisions, "cancelled"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.parametrize("completed", [False, True])
|
|
78
|
+
def test_inserted_patch_preserves_unmarked_activity(completed: bool) -> None:
|
|
79
|
+
outcome = replay(PatchInsertionWorkflow, old_activity(1, completed), [])
|
|
80
|
+
if completed:
|
|
81
|
+
assert len(outcome.commands) == 1
|
|
82
|
+
command = outcome.commands[0]
|
|
83
|
+
assert isinstance(command, CompleteWorkflow)
|
|
84
|
+
assert command.result == "recorded"
|
|
85
|
+
else:
|
|
86
|
+
# The old activity is already scheduled. Await it without duplicating
|
|
87
|
+
# that command or introducing the new patched activity.
|
|
88
|
+
assert outcome.commands == []
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_repeated_patch_emits_one_marker_on_new_history() -> None:
|
|
92
|
+
commands = replay(RepeatedPatchWorkflow, [], []).commands
|
|
93
|
+
assert [type(command) for command in commands] == [RecordVersionMarker, ScheduleActivity]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.mark.parametrize("repeat", [False, True])
|
|
97
|
+
@pytest.mark.parametrize("duplicates", [False, True])
|
|
98
|
+
def test_old_consistent_duplicate_markers_remain_replayable(repeat: bool, duplicates: bool) -> None:
|
|
99
|
+
history = [version_marker(1)]
|
|
100
|
+
if duplicates:
|
|
101
|
+
history.append(version_marker(2))
|
|
102
|
+
history += old_activity(3 if duplicates else 2)
|
|
103
|
+
commands = replay(RepeatedPatchWorkflow, history, [repeat]).commands
|
|
104
|
+
assert len(commands) == 1
|
|
105
|
+
assert isinstance(commands[0], CompleteWorkflow)
|
|
106
|
+
assert commands[0].result == [True, True, "recorded"]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.parametrize("repeat", [False, True])
|
|
110
|
+
def test_duplicate_markers_after_an_activity_remain_replayable(repeat: bool) -> None:
|
|
111
|
+
history = [version_marker(1)] + old_activity(2) + [version_marker(3)]
|
|
112
|
+
commands = replay(InterleavedPatchWorkflow, history, [repeat]).commands
|
|
113
|
+
assert len(commands) == 1
|
|
114
|
+
assert isinstance(commands[0], CompleteWorkflow)
|
|
115
|
+
assert commands[0].result == [True, True, "recorded"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_repeated_version_reuses_first_decision() -> None:
|
|
119
|
+
commands = replay(VersionRangeWorkflow, [], []).commands
|
|
120
|
+
assert [type(command) for command in commands] == [RecordVersionMarker, CompleteWorkflow]
|
|
121
|
+
assert commands[-1].result == [2, 2]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.mark.parametrize("arguments", [[3], [1, True]])
|
|
125
|
+
def test_repeated_version_rejects_incompatible_range_or_family(arguments: list[Any]) -> None:
|
|
126
|
+
with pytest.raises(NonDeterministicReplayError):
|
|
127
|
+
replay(VersionRangeWorkflow, [], arguments)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.parametrize("version", [-1, 2, True])
|
|
131
|
+
def test_conflicting_duplicate_marker_is_rejected(version: Any) -> None:
|
|
132
|
+
history = [version_marker(1), version_marker(2, version=version)] + old_activity(3)
|
|
133
|
+
with pytest.raises(NonDeterministicReplayError):
|
|
134
|
+
replay(RepeatedPatchWorkflow, history, [])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@pytest.mark.parametrize("marker_count", [0, 1, 2])
|
|
138
|
+
def test_patch_decisions_preserve_committed_cancellation_sequence(marker_count: int) -> None:
|
|
139
|
+
history = [version_marker(sequence) for sequence in range(1, marker_count + 1)]
|
|
140
|
+
history += old_activity(marker_count + 1, False)
|
|
141
|
+
history += [request(), cancellation_marker(marker_count + 1)]
|
|
142
|
+
commands = replay(PatchCancellationWorkflow, history, [], run_id="run-1").commands
|
|
143
|
+
assert len(commands) == 1
|
|
144
|
+
assert isinstance(commands[0], CompleteWorkflow)
|
|
145
|
+
assert commands[0].result == [[marker_count > 0, marker_count > 0], "cancelled"]
|
|
@@ -24,6 +24,7 @@ from tests.test_golden_history_replay import (
|
|
|
24
24
|
GoldenTimeoutWaitWorkflow,
|
|
25
25
|
GoldenVersionMarkerWorkflow,
|
|
26
26
|
)
|
|
27
|
+
from tests.test_patch_history import PatchCancellationWorkflow, PatchInsertionWorkflow, RepeatedPatchWorkflow
|
|
27
28
|
from tests.test_pending_operation_replay import PendingOperation
|
|
28
29
|
from tests.test_update_signal_condition_replay import (
|
|
29
30
|
PostConditionReceiversWorkflow,
|
|
@@ -266,6 +267,9 @@ WORKFLOWS = [
|
|
|
266
267
|
NestedParallelPathWorkflow,
|
|
267
268
|
ParallelMetadataProducerWorkflow,
|
|
268
269
|
ParallelResultBindingWorkflow,
|
|
270
|
+
PatchCancellationWorkflow,
|
|
271
|
+
PatchInsertionWorkflow,
|
|
272
|
+
RepeatedPatchWorkflow,
|
|
269
273
|
PendingOperation,
|
|
270
274
|
PagedRecordedSideEffectsWorkflow,
|
|
271
275
|
PostConditionReceiversWorkflow,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_cooperative_cancellation.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_prepared_local_activity.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/_sticky_workflow_cache.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.5.0 → durable_workflow-2.5.1}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.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.5.0 → durable_workflow-2.5.1}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.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
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.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
|
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cancellation_scope_authoring.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.5.0 → durable_workflow-2.5.1}/tests/test_committed_cancellation_scope_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_committed_cancellation_scope_history.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_committed_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cooperative_cancellation_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_cooperative_cancellation_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
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_prepared_local_activity_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_prepared_local_activity_policies.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.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.5.0 → durable_workflow-2.5.1}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_runtime_external_payload_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_scoped_run_cancellation_context.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
|
{durable_workflow-2.5.0 → durable_workflow-2.5.1}/tests/test_update_signal_condition_replay.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.5.0 → durable_workflow-2.5.1}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|