durable-workflow 2.5.2__tar.gz → 2.5.3__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.2/src/durable_workflow.egg-info → durable_workflow-2.5.3}/PKG-INFO +1 -1
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/pyproject.toml +3 -3
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/worker.py +30 -14
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/workflow.py +17 -10
- {durable_workflow-2.5.2 → durable_workflow-2.5.3/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
- durable_workflow-2.5.3/tests/test_condition_replay_drift.py +132 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_replay_regression_corpus.py +2 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_wait_condition.py +10 -15
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/LICENSE +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/README.md +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/setup.cfg +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_client.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_errors.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_invocable.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_metrics.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_queries.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_replay.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_schedules.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_serializer.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_signals.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_sleep.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_sync.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_updates.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_worker.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.5.2 → durable_workflow-2.5.3}/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.3"
|
|
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,8 +71,8 @@ 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.
|
|
74
|
+
product-train = "2.5.3"
|
|
75
|
+
registry-version = "2.5.3"
|
|
76
76
|
supported-server-versions = "2.5.16"
|
|
77
77
|
worker-protocol-version = "1.19"
|
|
78
78
|
control-plane-version = "2"
|
|
@@ -69,6 +69,7 @@ from .errors import (
|
|
|
69
69
|
DurableWorkflowError,
|
|
70
70
|
ExternalPayloadError,
|
|
71
71
|
InvalidArgument,
|
|
72
|
+
NonDeterministicReplayError,
|
|
72
73
|
NonRetryableError,
|
|
73
74
|
QueryFailed,
|
|
74
75
|
ServerError,
|
|
@@ -2502,20 +2503,35 @@ class Worker:
|
|
|
2502
2503
|
|
|
2503
2504
|
update_id = task.get("workflow_update_id")
|
|
2504
2505
|
if isinstance(update_id, str) and update_id:
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2506
|
+
try:
|
|
2507
|
+
update_command = apply_update(
|
|
2508
|
+
cls,
|
|
2509
|
+
history,
|
|
2510
|
+
start_input,
|
|
2511
|
+
update_id,
|
|
2512
|
+
workflow_id=task.get("workflow_id"),
|
|
2513
|
+
run_id=run_id,
|
|
2514
|
+
payload_codec=codec,
|
|
2515
|
+
external_storage=self.external_storage,
|
|
2516
|
+
external_storage_cache=self.external_storage_cache,
|
|
2517
|
+
prepare_local_activities=self._prepared_local_activities_supported,
|
|
2518
|
+
prepare_local_activity_groups=self._prepared_local_activity_groups_supported,
|
|
2519
|
+
local_activity_cancellation_policies=self._local_activity_cancellation_policies,
|
|
2520
|
+
)
|
|
2521
|
+
except NonDeterministicReplayError as exc:
|
|
2522
|
+
log.exception("workflow update task %s replay failed for run %s", task_id, run_id)
|
|
2523
|
+
try:
|
|
2524
|
+
await self.client.fail_workflow_task(
|
|
2525
|
+
task_id=task_id,
|
|
2526
|
+
lease_owner=self.worker_id,
|
|
2527
|
+
workflow_task_attempt=attempt,
|
|
2528
|
+
message=f"workflow replay failed before update {update_id!r}: {exc}",
|
|
2529
|
+
failure_type=type(exc).__name__,
|
|
2530
|
+
stack_trace=traceback.format_exc(),
|
|
2531
|
+
)
|
|
2532
|
+
except Exception as failure_error:
|
|
2533
|
+
log.warning("failed to report update replay failure: %s", failure_error)
|
|
2534
|
+
return None
|
|
2519
2535
|
command = update_command.to_server_command(
|
|
2520
2536
|
self.task_queue,
|
|
2521
2537
|
payload_codec=command_codec,
|
|
@@ -3067,6 +3067,8 @@ def apply_update(
|
|
|
3067
3067
|
prepare_local_activity_groups=prepare_local_activity_groups,
|
|
3068
3068
|
local_activity_cancellation_policies=local_activity_cancellation_policies,
|
|
3069
3069
|
)
|
|
3070
|
+
except NonDeterministicReplayError:
|
|
3071
|
+
raise
|
|
3070
3072
|
except Exception as exc:
|
|
3071
3073
|
return _fail_update_from_exception(
|
|
3072
3074
|
update_id,
|
|
@@ -5144,16 +5146,20 @@ def _replay_state(
|
|
|
5144
5146
|
for _ in range(boundary):
|
|
5145
5147
|
_apply_receiver(pending_receivers.pop(0))
|
|
5146
5148
|
|
|
5147
|
-
def _condition_wait_mismatch(
|
|
5149
|
+
def _condition_wait_mismatch(
|
|
5150
|
+
opened: Mapping[str, Any], cmd: WaitCondition,
|
|
5151
|
+
) -> NonDeterministicReplayError | None:
|
|
5152
|
+
step = recorded_wait_steps[wait_yield_count]
|
|
5153
|
+
identity = f"condition wait {opened.get('condition_wait_id')!r}"
|
|
5148
5154
|
opened_key = opened.get("condition_key")
|
|
5149
5155
|
if isinstance(opened_key, str) and opened_key != (cmd.condition_key or ""):
|
|
5150
|
-
return
|
|
5151
|
-
|
|
5152
|
-
|
|
5156
|
+
return NonDeterministicReplayError(
|
|
5157
|
+
step.workflow_sequence, "condition wait", step.event_types,
|
|
5158
|
+
detail=(
|
|
5159
|
+
f"condition_key_changed: {identity} wait_condition key changed during replay: "
|
|
5153
5160
|
f"history has {opened_key!r}, workflow yielded "
|
|
5154
5161
|
f"{cmd.condition_key!r}"
|
|
5155
5162
|
),
|
|
5156
|
-
exception_type="NonDeterministicWaitCondition",
|
|
5157
5163
|
)
|
|
5158
5164
|
|
|
5159
5165
|
opened_fingerprint = opened.get("condition_definition_fingerprint")
|
|
@@ -5161,13 +5167,14 @@ def _replay_state(
|
|
|
5161
5167
|
isinstance(opened_fingerprint, str)
|
|
5162
5168
|
and cmd.condition_definition_fingerprint != opened_fingerprint
|
|
5163
5169
|
):
|
|
5164
|
-
return
|
|
5165
|
-
|
|
5170
|
+
return NonDeterministicReplayError(
|
|
5171
|
+
step.workflow_sequence, "condition wait", step.event_types,
|
|
5172
|
+
detail=(
|
|
5173
|
+
f"condition_fingerprint_changed: {identity} "
|
|
5166
5174
|
"wait_condition predicate fingerprint changed during replay: "
|
|
5167
5175
|
f"history has {opened_fingerprint!r}, workflow yielded "
|
|
5168
5176
|
f"{cmd.condition_definition_fingerprint!r}"
|
|
5169
5177
|
),
|
|
5170
|
-
exception_type="NonDeterministicWaitCondition",
|
|
5171
5178
|
)
|
|
5172
5179
|
|
|
5173
5180
|
return None
|
|
@@ -6655,7 +6662,7 @@ def _replay_state(
|
|
|
6655
6662
|
next_value = True
|
|
6656
6663
|
continue
|
|
6657
6664
|
if next_opened_mismatch is not None:
|
|
6658
|
-
|
|
6665
|
+
raise next_opened_mismatch
|
|
6659
6666
|
step = _next_unconsumed_recorded_step()
|
|
6660
6667
|
if step is not None:
|
|
6661
6668
|
_assert_step_matches(cmd, step)
|
|
@@ -6707,7 +6714,7 @@ def _replay_state(
|
|
|
6707
6714
|
opened = wait_opened[wait_yield_count]
|
|
6708
6715
|
mismatch = _condition_wait_mismatch(opened, cmd)
|
|
6709
6716
|
if mismatch is not None:
|
|
6710
|
-
|
|
6717
|
+
raise mismatch
|
|
6711
6718
|
opened_id = opened.get("condition_wait_id")
|
|
6712
6719
|
if isinstance(opened_id, str):
|
|
6713
6720
|
resolution = wait_resolutions.get(opened_id)
|
|
@@ -62,6 +62,7 @@ tests/test_committed_cancellation_scope_descendants.py
|
|
|
62
62
|
tests/test_committed_cancellation_scope_groups.py
|
|
63
63
|
tests/test_committed_cancellation_scope_history.py
|
|
64
64
|
tests/test_committed_cancellation_scope_replay.py
|
|
65
|
+
tests/test_condition_replay_drift.py
|
|
65
66
|
tests/test_control_plane_parity_fixtures.py
|
|
66
67
|
tests/test_cooperative_cancellation.py
|
|
67
68
|
tests/test_cooperative_cancellation_client.py
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from durable_workflow import Worker, serializer, workflow
|
|
8
|
+
from durable_workflow.client import Client
|
|
9
|
+
from durable_workflow.errors import NonDeterministicReplayError, QueryFailed
|
|
10
|
+
from durable_workflow.workflow import WorkflowContext, apply_update, query_state, replay
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@workflow.defn(name="tests.replay.condition-identity")
|
|
14
|
+
class ConditionIdentityWorkflow:
|
|
15
|
+
@workflow.query("state")
|
|
16
|
+
def state(self) -> str:
|
|
17
|
+
return "waiting"
|
|
18
|
+
|
|
19
|
+
@workflow.update("approve")
|
|
20
|
+
def approve(self) -> str:
|
|
21
|
+
return "approved"
|
|
22
|
+
|
|
23
|
+
def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
|
|
24
|
+
yield ctx.wait_condition(lambda: False, key="approved")
|
|
25
|
+
return "done"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def condition_history(field: str) -> list[dict]:
|
|
29
|
+
return [{
|
|
30
|
+
"event_type": "ConditionWaitOpened",
|
|
31
|
+
"payload": {
|
|
32
|
+
"sequence": 7,
|
|
33
|
+
"condition_wait_id": "wait-original",
|
|
34
|
+
"condition_key": "approved",
|
|
35
|
+
field: "old-key" if field == "condition_key" else "sha256:different",
|
|
36
|
+
},
|
|
37
|
+
}]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("field", ["condition_key", "condition_definition_fingerprint"])
|
|
41
|
+
@pytest.mark.parametrize("consumer", ["replay", "query", "update"])
|
|
42
|
+
def test_condition_drift_is_not_a_business_failure(field: str, consumer: str) -> None:
|
|
43
|
+
history = condition_history(field)
|
|
44
|
+
error = QueryFailed if consumer == "query" else NonDeterministicReplayError
|
|
45
|
+
with pytest.raises(error) as captured:
|
|
46
|
+
if consumer == "query":
|
|
47
|
+
query_state(ConditionIdentityWorkflow, history, [], "state")
|
|
48
|
+
elif consumer == "update":
|
|
49
|
+
apply_update(ConditionIdentityWorkflow, history, [], "update-original")
|
|
50
|
+
else:
|
|
51
|
+
replay(ConditionIdentityWorkflow, history, [])
|
|
52
|
+
|
|
53
|
+
mismatch = captured.value.__cause__ if consumer == "query" else captured.value
|
|
54
|
+
assert isinstance(mismatch, NonDeterministicReplayError)
|
|
55
|
+
assert mismatch.workflow_sequence == 7
|
|
56
|
+
assert "wait-original" in str(mismatch)
|
|
57
|
+
assert "changed" in str(mismatch)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
@pytest.mark.parametrize("field", ["condition_key", "condition_definition_fingerprint"])
|
|
62
|
+
@pytest.mark.parametrize("consumer", ["workflow", "update"])
|
|
63
|
+
async def test_worker_reports_drift_once_without_a_completion(field: str, consumer: str) -> None:
|
|
64
|
+
client = AsyncMock(spec=Client)
|
|
65
|
+
worker = Worker(client, task_queue="condition-drift", workflows=[ConditionIdentityWorkflow])
|
|
66
|
+
task = {
|
|
67
|
+
"task_id": "task-original",
|
|
68
|
+
"workflow_id": "workflow-original",
|
|
69
|
+
"run_id": "run-original",
|
|
70
|
+
"workflow_type": "tests.replay.condition-identity",
|
|
71
|
+
"workflow_task_attempt": 2,
|
|
72
|
+
"history_events": condition_history(field),
|
|
73
|
+
"arguments": serializer.envelope([], codec="avro"),
|
|
74
|
+
"payload_codec": "avro",
|
|
75
|
+
}
|
|
76
|
+
if consumer == "update":
|
|
77
|
+
task["workflow_update_id"] = "update-original"
|
|
78
|
+
|
|
79
|
+
assert await worker._run_workflow_task_core(task) is None
|
|
80
|
+
|
|
81
|
+
client.complete_workflow_task.assert_not_awaited()
|
|
82
|
+
client.fail_workflow_task.assert_awaited_once()
|
|
83
|
+
report = client.fail_workflow_task.await_args.kwargs
|
|
84
|
+
assert report["task_id"] == "task-original"
|
|
85
|
+
assert report["workflow_task_attempt"] == 2
|
|
86
|
+
assert report["lease_owner"] == worker.worker_id
|
|
87
|
+
assert report["failure_type"] == "NonDeterministicReplayError"
|
|
88
|
+
assert "wait-original" in report["message"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_application_predicate_failure_remains_terminal() -> None:
|
|
92
|
+
@workflow.defn(name="tests.condition-predicate-failure")
|
|
93
|
+
class PredicateFailure:
|
|
94
|
+
def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
|
|
95
|
+
def predicate() -> bool:
|
|
96
|
+
raise ValueError("application failed")
|
|
97
|
+
|
|
98
|
+
yield ctx.wait_condition(predicate, key="approved")
|
|
99
|
+
|
|
100
|
+
outcome = replay(PredicateFailure, [], [])
|
|
101
|
+
assert len(outcome.commands) == 1
|
|
102
|
+
command = outcome.commands[0].to_server_command("condition-drift")
|
|
103
|
+
assert command["type"] == "fail_workflow"
|
|
104
|
+
assert command["exception_type"] == "ValueError"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.asyncio
|
|
108
|
+
@pytest.mark.parametrize("field", ["condition_key", "condition_definition_fingerprint"])
|
|
109
|
+
async def test_query_worker_reports_unavailable_state_without_changing_the_run(field: str) -> None:
|
|
110
|
+
client = AsyncMock(spec=Client)
|
|
111
|
+
worker = Worker(client, task_queue="condition-drift", workflows=[ConditionIdentityWorkflow])
|
|
112
|
+
outcome = await worker._run_query_task({
|
|
113
|
+
"query_task_id": "query-original",
|
|
114
|
+
"query_task_attempt": 2,
|
|
115
|
+
"workflow_id": "workflow-original",
|
|
116
|
+
"run_id": "run-original",
|
|
117
|
+
"workflow_type": "tests.replay.condition-identity",
|
|
118
|
+
"query_name": "state",
|
|
119
|
+
"history_events": condition_history(field),
|
|
120
|
+
"workflow_arguments": serializer.envelope([], codec="avro"),
|
|
121
|
+
"query_arguments": serializer.envelope([], codec="avro"),
|
|
122
|
+
"payload_codec": "avro",
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
assert outcome == "failed"
|
|
126
|
+
client.complete_query_task.assert_not_awaited()
|
|
127
|
+
client.complete_workflow_task.assert_not_awaited()
|
|
128
|
+
client.fail_workflow_task.assert_not_awaited()
|
|
129
|
+
client.fail_query_task.assert_awaited_once()
|
|
130
|
+
report = client.fail_query_task.await_args.kwargs
|
|
131
|
+
assert report["reason"] == "query_workflow_state_unavailable"
|
|
132
|
+
assert "wait-original" in report["message"]
|
|
@@ -17,6 +17,7 @@ from durable_workflow.workflow import (
|
|
|
17
17
|
commands_to_server_commands,
|
|
18
18
|
query_state,
|
|
19
19
|
)
|
|
20
|
+
from tests.test_condition_replay_drift import ConditionIdentityWorkflow
|
|
20
21
|
from tests.test_golden_history_replay import (
|
|
21
22
|
GoldenSagaCompensationWorkflow,
|
|
22
23
|
GoldenSignalWaitWorkflow,
|
|
@@ -291,6 +292,7 @@ WORKFLOWS = [
|
|
|
291
292
|
YieldedContinueAfterMetadataWorkflow,
|
|
292
293
|
]
|
|
293
294
|
WORKFLOW_TYPES = {str(getattr(workflow, "__workflow_name__", workflow.__name__)): workflow for workflow in WORKFLOWS}
|
|
295
|
+
WORKFLOW_TYPES["tests.replay.condition-identity"] = ConditionIdentityWorkflow
|
|
294
296
|
|
|
295
297
|
|
|
296
298
|
def _fixture_paths() -> list[Path]:
|
|
@@ -6,7 +6,6 @@ from durable_workflow import serializer, workflow
|
|
|
6
6
|
from durable_workflow.errors import NonDeterministicReplayError
|
|
7
7
|
from durable_workflow.workflow import (
|
|
8
8
|
CompleteWorkflow,
|
|
9
|
-
FailWorkflow,
|
|
10
9
|
WaitCondition,
|
|
11
10
|
WorkflowContext,
|
|
12
11
|
query_state,
|
|
@@ -1052,14 +1051,12 @@ class TestReplayWaitCondition:
|
|
|
1052
1051
|
},
|
|
1053
1052
|
]
|
|
1054
1053
|
|
|
1055
|
-
|
|
1054
|
+
with pytest.raises(NonDeterministicReplayError, match="key changed") as captured:
|
|
1055
|
+
replay(WaitUntilApproved, history, [])
|
|
1056
1056
|
|
|
1057
|
-
assert
|
|
1058
|
-
assert
|
|
1059
|
-
|
|
1060
|
-
assert isinstance(cmd, FailWorkflow)
|
|
1061
|
-
assert cmd.exception_type == "NonDeterministicWaitCondition"
|
|
1062
|
-
assert "key changed" in cmd.message
|
|
1057
|
+
assert captured.value.workflow_sequence == 1
|
|
1058
|
+
assert "old-key" in str(captured.value)
|
|
1059
|
+
assert "approved" in str(captured.value)
|
|
1063
1060
|
|
|
1064
1061
|
def test_replay_rejects_changed_condition_fingerprint(self) -> None:
|
|
1065
1062
|
initial = replay(WaitUntilApproved, [], [])
|
|
@@ -1077,14 +1074,12 @@ class TestReplayWaitCondition:
|
|
|
1077
1074
|
},
|
|
1078
1075
|
]
|
|
1079
1076
|
|
|
1080
|
-
|
|
1077
|
+
with pytest.raises(NonDeterministicReplayError, match="predicate fingerprint changed") as captured:
|
|
1078
|
+
replay(WaitUntilApproved, history, [])
|
|
1081
1079
|
|
|
1082
|
-
assert
|
|
1083
|
-
|
|
1084
|
-
assert
|
|
1085
|
-
assert cmd.exception_type == "NonDeterministicWaitCondition"
|
|
1086
|
-
assert "predicate fingerprint changed" in cmd.message
|
|
1087
|
-
assert fingerprint in cmd.message
|
|
1080
|
+
assert captured.value.workflow_sequence == 1
|
|
1081
|
+
assert "sha256:different" in str(captured.value)
|
|
1082
|
+
assert fingerprint in str(captured.value)
|
|
1088
1083
|
|
|
1089
1084
|
def test_replay_accepts_matching_condition_fingerprint(self) -> None:
|
|
1090
1085
|
initial = replay(WaitUntilApproved, [], [])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/_cooperative_cancellation.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/_prepared_local_activity.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/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.2 → durable_workflow-2.5.3}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/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.2 → durable_workflow-2.5.3}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/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
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/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.2 → durable_workflow-2.5.3}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/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.2 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_history.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_cooperative_cancellation_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/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
|
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_policies.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/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.2 → durable_workflow-2.5.3}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_runtime_external_payload_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/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.2 → durable_workflow-2.5.3}/tests/test_update_signal_condition_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_workflow_entrypoint_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.2 → durable_workflow-2.5.3}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|