durable-workflow 2.6.1__tar.gz → 2.6.2__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.6.1/src/durable_workflow.egg-info → durable_workflow-2.6.2}/PKG-INFO +1 -1
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/pyproject.toml +3 -3
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/__init__.py +2 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/activity.py +28 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/client.py +6 -1
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/errors.py +15 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/worker.py +34 -11
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/workflow.py +128 -15
- {durable_workflow-2.6.1 → durable_workflow-2.6.2/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/SOURCES.txt +3 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_replay_regression_corpus.py +30 -1
- durable_workflow-2.6.2/tests/test_sync_activities.py +197 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_worker.py +7 -3
- durable_workflow-2.6.2/tests/test_workflow_entry_arguments.py +141 -0
- durable_workflow-2.6.2/tests/test_workflow_handler_validation.py +150 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/LICENSE +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/README.md +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/setup.cfg +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_client.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_errors.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_invocable.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_metrics.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_queries.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_replay.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_replay_clock.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_schedules.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_serializer.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_signals.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_sleep.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_sync.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_updates.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_worker_observation.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.6.1 → durable_workflow-2.6.2}/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.6.
|
|
7
|
+
version = "2.6.2"
|
|
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.6.
|
|
75
|
-
registry-version = "2.6.
|
|
74
|
+
product-train = "2.6.2"
|
|
75
|
+
registry-version = "2.6.2"
|
|
76
76
|
supported-server-versions = "2.8.0"
|
|
77
77
|
worker-protocol-version = "1.20"
|
|
78
78
|
control-plane-version = "2"
|
|
@@ -103,6 +103,7 @@ from .errors import (
|
|
|
103
103
|
UpdateValidationFailed,
|
|
104
104
|
WorkflowAlreadyStarted,
|
|
105
105
|
WorkflowCancelled,
|
|
106
|
+
WorkflowEntryArgumentsMismatchError,
|
|
106
107
|
WorkflowFailed,
|
|
107
108
|
WorkflowNotFound,
|
|
108
109
|
WorkflowPayloadDecodeError,
|
|
@@ -319,6 +320,7 @@ __all__ = [
|
|
|
319
320
|
"WorkflowHandle",
|
|
320
321
|
"WorkflowList",
|
|
321
322
|
"WorkflowPayloadDecodeError",
|
|
323
|
+
"WorkflowEntryArgumentsMismatchError",
|
|
322
324
|
"WorkflowRun",
|
|
323
325
|
"WorkflowRunList",
|
|
324
326
|
"WorkflowStreamAppendItem",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Activity decorator, registry, and execution context."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import asyncio
|
|
4
5
|
import contextvars
|
|
5
6
|
from collections.abc import Awaitable, Callable
|
|
6
7
|
from dataclasses import dataclass
|
|
@@ -44,6 +45,10 @@ class ActivityContext:
|
|
|
44
45
|
self._client = client
|
|
45
46
|
self._heartbeat_callback = heartbeat_callback
|
|
46
47
|
self._cancel_requested = False
|
|
48
|
+
try:
|
|
49
|
+
self._event_loop: asyncio.AbstractEventLoop | None = asyncio.get_running_loop()
|
|
50
|
+
except RuntimeError:
|
|
51
|
+
self._event_loop = None
|
|
47
52
|
|
|
48
53
|
@property
|
|
49
54
|
def info(self) -> ActivityInfo:
|
|
@@ -55,6 +60,29 @@ class ActivityContext:
|
|
|
55
60
|
"""``True`` once the server has signalled that the owning workflow cancelled this activity."""
|
|
56
61
|
return self._cancel_requested
|
|
57
62
|
|
|
63
|
+
def heartbeat_sync(self, details: dict[str, Any] | None = None) -> None:
|
|
64
|
+
"""Heartbeat from a synchronous activity thread and observe cancellation.
|
|
65
|
+
|
|
66
|
+
The request runs on the worker's event loop using the original attempt
|
|
67
|
+
and client. Async handlers must use ``await heartbeat()`` instead.
|
|
68
|
+
"""
|
|
69
|
+
loop = self._event_loop
|
|
70
|
+
if loop is None or not loop.is_running():
|
|
71
|
+
raise RuntimeError("heartbeat_sync requires a running worker event loop")
|
|
72
|
+
try:
|
|
73
|
+
caller_loop = asyncio.get_running_loop()
|
|
74
|
+
except RuntimeError:
|
|
75
|
+
caller_loop = None
|
|
76
|
+
if caller_loop is loop:
|
|
77
|
+
raise RuntimeError("use await activity.context().heartbeat() on the worker event loop")
|
|
78
|
+
heartbeat = self.heartbeat(details)
|
|
79
|
+
try:
|
|
80
|
+
pending = asyncio.run_coroutine_threadsafe(heartbeat, loop)
|
|
81
|
+
except RuntimeError:
|
|
82
|
+
heartbeat.close()
|
|
83
|
+
raise
|
|
84
|
+
pending.result()
|
|
85
|
+
|
|
58
86
|
async def heartbeat(self, details: dict[str, Any] | None = None) -> None:
|
|
59
87
|
"""Report liveness to the server and check for a cancellation request.
|
|
60
88
|
|
|
@@ -5312,15 +5312,20 @@ class Client:
|
|
|
5312
5312
|
message: str,
|
|
5313
5313
|
failure_type: str | None = None,
|
|
5314
5314
|
stack_trace: str | None = None,
|
|
5315
|
+
failure_reason: str | None = None,
|
|
5315
5316
|
) -> Any:
|
|
5316
|
-
"""Report a workflow task failure
|
|
5317
|
+
"""Report a workflow task failure without closing the run.
|
|
5317
5318
|
|
|
5318
5319
|
Worker-plane endpoint. Task failures (e.g. non-determinism) are
|
|
5319
5320
|
distinct from workflow failures (``FailWorkflow`` commands).
|
|
5321
|
+
The runtime may block incompatible replay until compatible code and
|
|
5322
|
+
a supported repair are available instead of automatically retrying it.
|
|
5320
5323
|
"""
|
|
5321
5324
|
failure: dict[str, Any] = {"message": message}
|
|
5322
5325
|
if failure_type is not None:
|
|
5323
5326
|
failure["type"] = failure_type
|
|
5327
|
+
if failure_reason is not None:
|
|
5328
|
+
failure["reason"] = failure_reason
|
|
5324
5329
|
if stack_trace is not None:
|
|
5325
5330
|
failure["stack_trace"] = stack_trace
|
|
5326
5331
|
body: dict[str, Any] = {
|
|
@@ -465,6 +465,21 @@ class WorkflowPayloadDecodeError(DurableWorkflowError):
|
|
|
465
465
|
self.exception_type = exception_type
|
|
466
466
|
|
|
467
467
|
|
|
468
|
+
class WorkflowEntryArgumentsMismatchError(DurableWorkflowError):
|
|
469
|
+
"""Recorded input cannot bind to the deployed entry method before it runs."""
|
|
470
|
+
|
|
471
|
+
reason = "workflow_entry_arguments_mismatch"
|
|
472
|
+
|
|
473
|
+
def __init__(self, workflow_type: str, binding_reason: str) -> None:
|
|
474
|
+
self.workflow_type = workflow_type
|
|
475
|
+
self.entry_method = "run"
|
|
476
|
+
self.binding_reason = binding_reason
|
|
477
|
+
super().__init__(
|
|
478
|
+
f"{self.reason}: {workflow_type}.run cannot bind the recorded arguments "
|
|
479
|
+
f"({binding_reason}). Restore compatible code and repair the workflow task."
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
468
483
|
class NonDeterministicReplayError(DurableWorkflowError):
|
|
469
484
|
"""Current workflow code yielded a command shape that does not match history."""
|
|
470
485
|
|
|
@@ -79,6 +79,7 @@ from .errors import (
|
|
|
79
79
|
ServerError,
|
|
80
80
|
UpdateRejected,
|
|
81
81
|
UpdateValidationFailed,
|
|
82
|
+
WorkflowEntryArgumentsMismatchError,
|
|
82
83
|
)
|
|
83
84
|
from .external_storage import ExternalPayloadCache, ExternalStorageDriver, store_external_payload
|
|
84
85
|
from .interceptors import (
|
|
@@ -108,12 +109,13 @@ from .workflow import (
|
|
|
108
109
|
StartChildWorkflow,
|
|
109
110
|
UpsertMemo,
|
|
110
111
|
_prepare_query_state,
|
|
112
|
+
_prepare_update_validation,
|
|
113
|
+
_UpdateValidationReplayOptions,
|
|
111
114
|
_validate_workflow_entrypoint,
|
|
112
115
|
_workflow_handler_registries,
|
|
113
116
|
apply_update,
|
|
114
117
|
commands_to_server_commands,
|
|
115
118
|
replay,
|
|
116
|
-
validate_update,
|
|
117
119
|
)
|
|
118
120
|
|
|
119
121
|
log = logging.getLogger("durable_workflow.worker")
|
|
@@ -136,6 +138,7 @@ _WORKFLOW_COMMAND_CAPABILITIES = (
|
|
|
136
138
|
TYPED_SEARCH_ATTRIBUTES_CAPABILITY,
|
|
137
139
|
DURABLE_SELECTION_CAPABILITY,
|
|
138
140
|
LOCAL_ACTIVITIES_CAPABILITY,
|
|
141
|
+
"workflow_entry_arguments_validation",
|
|
139
142
|
)
|
|
140
143
|
|
|
141
144
|
_WORKFLOW_WORK_TASK_KINDS = ("workflow", "update_validation")
|
|
@@ -2619,6 +2622,9 @@ class Worker:
|
|
|
2619
2622
|
return None
|
|
2620
2623
|
except Exception as e:
|
|
2621
2624
|
log.exception("replay failed")
|
|
2625
|
+
failure_details = (
|
|
2626
|
+
{"failure_reason": e.reason} if isinstance(e, WorkflowEntryArgumentsMismatchError) else {}
|
|
2627
|
+
)
|
|
2622
2628
|
try:
|
|
2623
2629
|
await self.client.fail_workflow_task(
|
|
2624
2630
|
task_id=task_id,
|
|
@@ -2627,6 +2633,7 @@ class Worker:
|
|
|
2627
2633
|
message=f"replay failed: {e}",
|
|
2628
2634
|
failure_type=type(e).__name__,
|
|
2629
2635
|
stack_trace=traceback.format_exc(),
|
|
2636
|
+
**failure_details,
|
|
2630
2637
|
)
|
|
2631
2638
|
except Exception as fe:
|
|
2632
2639
|
log.warning("failed to report replay failure: %s", fe)
|
|
@@ -3065,7 +3072,9 @@ class Worker:
|
|
|
3065
3072
|
if self._cooperative_cancellation_supported:
|
|
3066
3073
|
result = await self._execute_cooperative_remote_callable(task, fn, tuple(args), act_ctx.info)
|
|
3067
3074
|
else:
|
|
3068
|
-
result = await self._execute_activity_callable(
|
|
3075
|
+
result = await self._execute_activity_callable(
|
|
3076
|
+
task, activity_type, tuple(args), fn, run_sync_in_thread=True, remote=True,
|
|
3077
|
+
)
|
|
3069
3078
|
except _RemoteActivityExecutionAborted as error:
|
|
3070
3079
|
log.warning("remote activity %s claim abandoned: %s", task_id, error)
|
|
3071
3080
|
return "claim_aborted"
|
|
@@ -3859,21 +3868,24 @@ class Worker:
|
|
|
3859
3868
|
codec=codec,
|
|
3860
3869
|
context="workflow update validation arguments",
|
|
3861
3870
|
)
|
|
3862
|
-
|
|
3871
|
+
invoke = _prepare_update_validation(
|
|
3863
3872
|
workflow_cls,
|
|
3864
3873
|
history,
|
|
3865
3874
|
start_input,
|
|
3866
3875
|
update_name,
|
|
3867
3876
|
update_args,
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3877
|
+
replay_options=_UpdateValidationReplayOptions(
|
|
3878
|
+
workflow_id=task.get("workflow_id"),
|
|
3879
|
+
run_id=task.get("run_id", ""),
|
|
3880
|
+
payload_codec=codec,
|
|
3881
|
+
external_storage=self.external_storage,
|
|
3882
|
+
external_storage_cache=self.external_storage_cache,
|
|
3883
|
+
prepare_local_activities=self._prepared_local_activities_supported,
|
|
3884
|
+
prepare_local_activity_groups=self._prepared_local_activity_groups_supported,
|
|
3885
|
+
local_activity_cancellation_policies=self._local_activity_cancellation_policies,
|
|
3886
|
+
),
|
|
3876
3887
|
)
|
|
3888
|
+
result = invoke()
|
|
3877
3889
|
if inspect.isawaitable(result):
|
|
3878
3890
|
result = await result
|
|
3879
3891
|
if result is False:
|
|
@@ -4506,6 +4518,17 @@ class Worker:
|
|
|
4506
4518
|
)
|
|
4507
4519
|
await asyncio.gather(*in_flight, return_exceptions=True)
|
|
4508
4520
|
|
|
4521
|
+
# Cancelling the asyncio await cannot stop a running Python thread.
|
|
4522
|
+
# Keep registration and capacity until the actual callback has returned.
|
|
4523
|
+
while any(not future.done() for future in self._remote_activity_threads):
|
|
4524
|
+
remaining = self._remaining_shutdown_time(deadline)
|
|
4525
|
+
if remaining <= 0:
|
|
4526
|
+
raise RuntimeError(
|
|
4527
|
+
"worker shutdown has unconfirmed activity thread stop(s); "
|
|
4528
|
+
"the worker registration remains active"
|
|
4529
|
+
)
|
|
4530
|
+
await asyncio.sleep(min(0.01, remaining))
|
|
4531
|
+
|
|
4509
4532
|
if self._local_activity_executor is not None:
|
|
4510
4533
|
self._local_activity_executor.shutdown(wait=False, cancel_futures=True)
|
|
4511
4534
|
if self._remote_activity_executor is not None:
|
|
@@ -66,6 +66,7 @@ from .errors import (
|
|
|
66
66
|
QueryFailed,
|
|
67
67
|
SagaCompensationFailed,
|
|
68
68
|
WorkflowCancelled,
|
|
69
|
+
WorkflowEntryArgumentsMismatchError,
|
|
69
70
|
WorkflowPayloadDecodeError,
|
|
70
71
|
)
|
|
71
72
|
from .external_storage import ExternalPayloadCache, ExternalStorageDriver
|
|
@@ -137,6 +138,21 @@ def _workflow_handler_registries(
|
|
|
137
138
|
wire_name = getattr(member, f"__{kind}_name__", None)
|
|
138
139
|
if not isinstance(wire_name, str) or not wire_name:
|
|
139
140
|
continue
|
|
141
|
+
if not callable(member):
|
|
142
|
+
raise InvalidWorkflowDefinition(f"workflow {cls.__qualname__}.{attr} handler must be callable")
|
|
143
|
+
target = inspect.unwrap(member)
|
|
144
|
+
if not inspect.isroutine(target) and callable(target):
|
|
145
|
+
target = inspect.unwrap(target.__call__)
|
|
146
|
+
if (
|
|
147
|
+
(inspect.iscoroutinefunction(target) and kind not in {"query", "update_validator"})
|
|
148
|
+
or inspect.isasyncgenfunction(target)
|
|
149
|
+
or inspect.isgeneratorfunction(target)
|
|
150
|
+
):
|
|
151
|
+
name = getattr(cls, "__workflow_name__", cls.__name__)
|
|
152
|
+
raise InvalidWorkflowDefinition(
|
|
153
|
+
f"unsupported_workflow_handler: workflow {name!r} ({cls.__qualname__}.{attr}) "
|
|
154
|
+
f"{kind} handler {wire_name!r} must be synchronous; yield durable commands from run()."
|
|
155
|
+
)
|
|
140
156
|
if kind == "signal" and wire_name == MESSAGE_STREAM_SIGNAL:
|
|
141
157
|
raise ValueError(f"signal name {wire_name!r} is reserved by the workflow runtime")
|
|
142
158
|
if wire_name in registry:
|
|
@@ -148,6 +164,28 @@ def _workflow_handler_registries(
|
|
|
148
164
|
return handlers["signal"], handlers["query"], handlers["update"], handlers["update_validator"]
|
|
149
165
|
|
|
150
166
|
|
|
167
|
+
def _invoke_workflow_handler(
|
|
168
|
+
handler: Callable[..., Any], args: list[Any], cls: type, method: str, *, allow_async: bool = False,
|
|
169
|
+
) -> Any:
|
|
170
|
+
return _validate_workflow_handler_result(handler(*args), cls, method, allow_async=allow_async)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _validate_workflow_handler_result(result: Any, cls: type, method: str, *, allow_async: bool = False) -> Any:
|
|
174
|
+
if (
|
|
175
|
+
(inspect.isawaitable(result) and not allow_async)
|
|
176
|
+
or inspect.isasyncgen(result)
|
|
177
|
+
or inspect.isgenerator(result)
|
|
178
|
+
):
|
|
179
|
+
if inspect.iscoroutine(result) or inspect.isgenerator(result):
|
|
180
|
+
result.close()
|
|
181
|
+
name = getattr(cls, "__workflow_name__", cls.__name__)
|
|
182
|
+
raise InvalidWorkflowDefinition(
|
|
183
|
+
f"unsupported_workflow_handler_result: workflow {name!r} ({cls.__qualname__}.{method}) "
|
|
184
|
+
"returned an async or generator value; handlers must return synchronous values."
|
|
185
|
+
)
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
151
189
|
def defn(*, name: str) -> Callable[[type[_WorkflowT]], type[_WorkflowT]]:
|
|
152
190
|
"""Register a class as a workflow type under a language-neutral name.
|
|
153
191
|
|
|
@@ -3132,7 +3170,9 @@ def _prepare_query_state(
|
|
|
3132
3170
|
|
|
3133
3171
|
def invoke() -> Any:
|
|
3134
3172
|
try:
|
|
3135
|
-
return
|
|
3173
|
+
return _invoke_workflow_handler(
|
|
3174
|
+
handler, list(args) if args is not None else [], workflow_cls, method_name, allow_async=True,
|
|
3175
|
+
)
|
|
3136
3176
|
except QueryFailed:
|
|
3137
3177
|
raise
|
|
3138
3178
|
except Exception as exc:
|
|
@@ -3162,7 +3202,7 @@ def query_state(
|
|
|
3162
3202
|
Server-routed Worker execution separately checks its original query claim
|
|
3163
3203
|
between replay and handler invocation. Direct local use has no Server lease.
|
|
3164
3204
|
"""
|
|
3165
|
-
|
|
3205
|
+
result = _prepare_query_state(
|
|
3166
3206
|
workflow_cls, history_events, start_input, query_name, args,
|
|
3167
3207
|
workflow_id=workflow_id, run_id=run_id, payload_codec=payload_codec,
|
|
3168
3208
|
external_storage=external_storage, external_storage_cache=external_storage_cache,
|
|
@@ -3170,6 +3210,11 @@ def query_state(
|
|
|
3170
3210
|
prepare_local_activity_groups=prepare_local_activity_groups,
|
|
3171
3211
|
local_activity_cancellation_policies=local_activity_cancellation_policies,
|
|
3172
3212
|
)()
|
|
3213
|
+
try:
|
|
3214
|
+
registry: dict[str, str] = getattr(workflow_cls, "__workflow_queries__", {}) or {}
|
|
3215
|
+
return _validate_workflow_handler_result(result, workflow_cls, registry.get(query_name, query_name))
|
|
3216
|
+
except InvalidWorkflowDefinition as exc:
|
|
3217
|
+
raise QueryFailed(str(exc)) from exc
|
|
3173
3218
|
|
|
3174
3219
|
|
|
3175
3220
|
def apply_update(
|
|
@@ -3275,11 +3320,24 @@ def apply_update(
|
|
|
3275
3320
|
)
|
|
3276
3321
|
|
|
3277
3322
|
try:
|
|
3278
|
-
|
|
3323
|
+
result = _invoke_workflow_handler(handler, args, workflow_cls, method_name)
|
|
3324
|
+
return CompleteUpdate(update_id=update_id, result=result)
|
|
3279
3325
|
except Exception as exc:
|
|
3280
3326
|
return _fail_update_from_exception(update_id, "update handler failed", exc)
|
|
3281
3327
|
|
|
3282
3328
|
|
|
3329
|
+
@dataclass(frozen=True)
|
|
3330
|
+
class _UpdateValidationReplayOptions:
|
|
3331
|
+
workflow_id: str | None = None
|
|
3332
|
+
run_id: str = ""
|
|
3333
|
+
payload_codec: str | None = None
|
|
3334
|
+
external_storage: ExternalStorageDriver | None = None
|
|
3335
|
+
external_storage_cache: ExternalPayloadCache | None = None
|
|
3336
|
+
prepare_local_activities: bool = False
|
|
3337
|
+
prepare_local_activity_groups: bool = False
|
|
3338
|
+
local_activity_cancellation_policies: tuple[str, ...] = ()
|
|
3339
|
+
|
|
3340
|
+
|
|
3283
3341
|
def validate_update(
|
|
3284
3342
|
workflow_cls: type,
|
|
3285
3343
|
history_events: Iterable[dict[str, Any]],
|
|
@@ -3296,13 +3354,42 @@ def validate_update(
|
|
|
3296
3354
|
prepare_local_activity_groups: bool = False,
|
|
3297
3355
|
local_activity_cancellation_policies: tuple[str, ...] = (),
|
|
3298
3356
|
) -> Any:
|
|
3357
|
+
"""Replay and invoke a validator synchronously, refusing asynchronous results."""
|
|
3358
|
+
from .errors import UpdateRejected
|
|
3359
|
+
|
|
3360
|
+
result = _prepare_update_validation(
|
|
3361
|
+
workflow_cls, history_events, start_input, update_name, args,
|
|
3362
|
+
replay_options=_UpdateValidationReplayOptions(
|
|
3363
|
+
workflow_id=workflow_id, run_id=run_id, payload_codec=payload_codec,
|
|
3364
|
+
external_storage=external_storage, external_storage_cache=external_storage_cache,
|
|
3365
|
+
prepare_local_activities=prepare_local_activities,
|
|
3366
|
+
prepare_local_activity_groups=prepare_local_activity_groups,
|
|
3367
|
+
local_activity_cancellation_policies=local_activity_cancellation_policies,
|
|
3368
|
+
),
|
|
3369
|
+
)()
|
|
3370
|
+
try:
|
|
3371
|
+
registry: dict[str, str] = getattr(workflow_cls, "__workflow_update_validators__", {}) or {}
|
|
3372
|
+
return _validate_workflow_handler_result(result, workflow_cls, registry.get(update_name, update_name))
|
|
3373
|
+
except InvalidWorkflowDefinition as exc:
|
|
3374
|
+
raise UpdateRejected(str(exc), exception_type=type(exc).__name__) from exc
|
|
3375
|
+
|
|
3376
|
+
|
|
3377
|
+
def _prepare_update_validation(
|
|
3378
|
+
workflow_cls: type,
|
|
3379
|
+
history_events: Iterable[dict[str, Any]],
|
|
3380
|
+
start_input: list[Any],
|
|
3381
|
+
update_name: str,
|
|
3382
|
+
args: list[Any],
|
|
3383
|
+
*,
|
|
3384
|
+
replay_options: _UpdateValidationReplayOptions,
|
|
3385
|
+
) -> Callable[[], Any]:
|
|
3299
3386
|
"""Replay state and invoke only the declared pre-accept update validator.
|
|
3300
3387
|
|
|
3301
3388
|
The replayed instance is discarded after validation. This helper does not
|
|
3302
3389
|
run the update handler and does not emit workflow commands, so the server
|
|
3303
3390
|
remains the sole authority that can cross the accepted-state boundary.
|
|
3304
3391
|
"""
|
|
3305
|
-
from .errors import
|
|
3392
|
+
from .errors import UpdateValidationFailed
|
|
3306
3393
|
|
|
3307
3394
|
events = list(history_events)
|
|
3308
3395
|
try:
|
|
@@ -3310,14 +3397,14 @@ def validate_update(
|
|
|
3310
3397
|
workflow_cls,
|
|
3311
3398
|
events,
|
|
3312
3399
|
start_input,
|
|
3313
|
-
workflow_id=workflow_id,
|
|
3314
|
-
run_id=run_id,
|
|
3315
|
-
payload_codec=payload_codec,
|
|
3316
|
-
external_storage=external_storage,
|
|
3317
|
-
external_storage_cache=external_storage_cache,
|
|
3318
|
-
prepare_local_activities=prepare_local_activities,
|
|
3319
|
-
prepare_local_activity_groups=prepare_local_activity_groups,
|
|
3320
|
-
local_activity_cancellation_policies=local_activity_cancellation_policies,
|
|
3400
|
+
workflow_id=replay_options.workflow_id,
|
|
3401
|
+
run_id=replay_options.run_id,
|
|
3402
|
+
payload_codec=replay_options.payload_codec,
|
|
3403
|
+
external_storage=replay_options.external_storage,
|
|
3404
|
+
external_storage_cache=replay_options.external_storage_cache,
|
|
3405
|
+
prepare_local_activities=replay_options.prepare_local_activities,
|
|
3406
|
+
prepare_local_activity_groups=replay_options.prepare_local_activity_groups,
|
|
3407
|
+
local_activity_cancellation_policies=replay_options.local_activity_cancellation_policies,
|
|
3321
3408
|
)
|
|
3322
3409
|
except Exception as exc:
|
|
3323
3410
|
raise UpdateValidationFailed(
|
|
@@ -3348,8 +3435,20 @@ def validate_update(
|
|
|
3348
3435
|
if validator is None:
|
|
3349
3436
|
raise UpdateValidationFailed(f"update validator {update_name!r} is not available")
|
|
3350
3437
|
|
|
3438
|
+
return lambda: _invoke_update_validator(validator, args, workflow_cls, validator_name, update_name)
|
|
3439
|
+
|
|
3440
|
+
|
|
3441
|
+
def _invoke_update_validator(
|
|
3442
|
+
validator: Callable[..., Any],
|
|
3443
|
+
args: list[Any],
|
|
3444
|
+
workflow_cls: type,
|
|
3445
|
+
validator_name: str,
|
|
3446
|
+
update_name: str,
|
|
3447
|
+
) -> Any:
|
|
3448
|
+
from .errors import InvalidArgument, UpdateRejected
|
|
3449
|
+
|
|
3351
3450
|
try:
|
|
3352
|
-
result = validator
|
|
3451
|
+
result = _invoke_workflow_handler(validator, list(args), workflow_cls, validator_name, allow_async=True)
|
|
3353
3452
|
if result is False:
|
|
3354
3453
|
raise ValueError(f"update validator {update_name!r} returned false")
|
|
3355
3454
|
return result
|
|
@@ -4239,6 +4338,7 @@ def _replay_state(
|
|
|
4239
4338
|
allow_cancellation_scope_delivery: bool = False,
|
|
4240
4339
|
) -> _ReplayState:
|
|
4241
4340
|
_validate_workflow_entrypoint(workflow_cls)
|
|
4341
|
+
_workflow_handler_registries(workflow_cls)
|
|
4242
4342
|
if allow_cancellation_scope_delivery and not allow_cancellation_scope_authoring:
|
|
4243
4343
|
raise LocalActivityExecutionAborted("scope delivery requires canonical scope authoring")
|
|
4244
4344
|
if prepare_local_activity_groups and not prepare_local_activities:
|
|
@@ -5284,7 +5384,7 @@ def _replay_state(
|
|
|
5284
5384
|
return
|
|
5285
5385
|
raise TypeError(f"update handler {receiver.name!r} is not available")
|
|
5286
5386
|
ctx.logger._set_replaying(True)
|
|
5287
|
-
handler
|
|
5387
|
+
_invoke_workflow_handler(handler, receiver.args, workflow_cls, method_name)
|
|
5288
5388
|
|
|
5289
5389
|
def _receiver_due(receiver: _PendingReceiver, *, before_consuming_result: bool) -> bool:
|
|
5290
5390
|
if receiver.after_cancellation_delivery and not cancellation_consumed:
|
|
@@ -5386,8 +5486,21 @@ def _replay_state(
|
|
|
5386
5486
|
result_cursor = 0
|
|
5387
5487
|
pending_step_cursor = 0
|
|
5388
5488
|
wait_yield_count = 0
|
|
5489
|
+
entry = instance.run
|
|
5490
|
+
workflow_type = str(getattr(workflow_cls, "__workflow_name__", workflow_cls.__name__))
|
|
5491
|
+
try:
|
|
5492
|
+
entry_signature = inspect.signature(entry)
|
|
5493
|
+
except (TypeError, ValueError):
|
|
5494
|
+
raise WorkflowEntryArgumentsMismatchError(workflow_type, "signature_unavailable") from None
|
|
5495
|
+
try:
|
|
5496
|
+
entry_signature.bind(ctx, *start_input)
|
|
5497
|
+
except TypeError:
|
|
5498
|
+
raise WorkflowEntryArgumentsMismatchError(workflow_type, "argument_binding_failed") from None
|
|
5499
|
+
|
|
5500
|
+
# Binding happens outside the application-error path. A TypeError raised
|
|
5501
|
+
# by a successfully bound body is still an actual workflow failure.
|
|
5389
5502
|
try:
|
|
5390
|
-
gen =
|
|
5503
|
+
gen = entry(ctx, *start_input)
|
|
5391
5504
|
except NonDeterministicReplayError:
|
|
5392
5505
|
raise
|
|
5393
5506
|
except LocalActivityExecutionAborted:
|
|
@@ -120,6 +120,7 @@ tests/test_standalone_activity_client.py
|
|
|
120
120
|
tests/test_sticky_execution.py
|
|
121
121
|
tests/test_storage_admission.py
|
|
122
122
|
tests/test_sync.py
|
|
123
|
+
tests/test_sync_activities.py
|
|
123
124
|
tests/test_testing_harness.py
|
|
124
125
|
tests/test_update_signal_condition_replay.py
|
|
125
126
|
tests/test_updates.py
|
|
@@ -127,7 +128,9 @@ tests/test_wait_condition.py
|
|
|
127
128
|
tests/test_worker.py
|
|
128
129
|
tests/test_worker_observation.py
|
|
129
130
|
tests/test_worker_session.py
|
|
131
|
+
tests/test_workflow_entry_arguments.py
|
|
130
132
|
tests/test_workflow_entrypoint_validation.py
|
|
133
|
+
tests/test_workflow_handler_validation.py
|
|
131
134
|
tests/test_workflow_result_timeout.py
|
|
132
135
|
tests/test_workflow_streams.py
|
|
133
136
|
tests/test_workflow_updates_conformance.py
|
|
@@ -12,7 +12,12 @@ import pytest
|
|
|
12
12
|
|
|
13
13
|
from durable_workflow import Replayer, Worker, serializer, workflow
|
|
14
14
|
from durable_workflow.client import PROTOCOL_VERSION, Client, WorkflowExecution, WorkflowStreamAppendItem
|
|
15
|
-
from durable_workflow.errors import
|
|
15
|
+
from durable_workflow.errors import (
|
|
16
|
+
DurableWorkflowError,
|
|
17
|
+
NonDeterministicReplayError,
|
|
18
|
+
WorkflowCancelled,
|
|
19
|
+
WorkflowPayloadDecodeError,
|
|
20
|
+
)
|
|
16
21
|
from durable_workflow.replay_conformance import compose_report
|
|
17
22
|
from durable_workflow.workflow import (
|
|
18
23
|
LocalActivityExecutionAborted,
|
|
@@ -282,6 +287,23 @@ class UnsupportedAsyncWorkflow:
|
|
|
282
287
|
return "body was never executed"
|
|
283
288
|
|
|
284
289
|
|
|
290
|
+
class UnsupportedAsyncSignalWorkflow:
|
|
291
|
+
__workflow_name__ = "tests.replay.unsupported-async-signal"
|
|
292
|
+
|
|
293
|
+
@workflow.signal("change")
|
|
294
|
+
async def receive(self) -> None:
|
|
295
|
+
raise AssertionError("unsupported signal body must never run")
|
|
296
|
+
|
|
297
|
+
def run(self, ctx: WorkflowContext) -> str:
|
|
298
|
+
return "done"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@workflow.defn(name="tests.replay.entry-arguments-required")
|
|
302
|
+
class RequiredEntryArgumentWorkflow:
|
|
303
|
+
def run(self, ctx: WorkflowContext, amount: int, required: int) -> int:
|
|
304
|
+
raise AssertionError("incompatible entry body must never run")
|
|
305
|
+
|
|
306
|
+
|
|
285
307
|
@workflow.defn(name="tests.replay.nexus-target")
|
|
286
308
|
class NexusTargetWorkflow:
|
|
287
309
|
def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
|
|
@@ -323,6 +345,7 @@ WORKFLOWS = [
|
|
|
323
345
|
NexusTerminalDeliveryWorkflow,
|
|
324
346
|
NexusTargetWorkflow,
|
|
325
347
|
UnsupportedAsyncWorkflow,
|
|
348
|
+
UnsupportedAsyncSignalWorkflow,
|
|
326
349
|
ChildPolicyAuthorWorkflow,
|
|
327
350
|
ColdReplacementSatisfiedConditionWorkflow,
|
|
328
351
|
CooperativeReopenedConditionCleanupWorkflow,
|
|
@@ -356,6 +379,7 @@ WORKFLOWS = [
|
|
|
356
379
|
]
|
|
357
380
|
WORKFLOW_TYPES = {str(getattr(workflow, "__workflow_name__", workflow.__name__)): workflow for workflow in WORKFLOWS}
|
|
358
381
|
WORKFLOW_TYPES["tests.replay.condition-identity"] = ConditionIdentityWorkflow
|
|
382
|
+
WORKFLOW_TYPES["tests.replay.entry-arguments-required"] = RequiredEntryArgumentWorkflow
|
|
359
383
|
|
|
360
384
|
|
|
361
385
|
def _fixture_paths() -> list[Path]:
|
|
@@ -558,6 +582,11 @@ def test_checked_in_replay_regression_corpus_uses_official_replayer(
|
|
|
558
582
|
message = expected_replay_error.get("message_contains")
|
|
559
583
|
assert isinstance(message, str) and message
|
|
560
584
|
assert expected.get("command_sequence") == []
|
|
585
|
+
if expected_replay_error.get("type") == "WorkflowEntryArgumentsMismatchError":
|
|
586
|
+
with pytest.raises(DurableWorkflowError, match=message) as binding_error:
|
|
587
|
+
_execute_fixture(fixture)
|
|
588
|
+
assert type(binding_error.value).__name__ == "WorkflowEntryArgumentsMismatchError"
|
|
589
|
+
return
|
|
561
590
|
if expected_replay_error.get("type") == "LocalActivityExecutionAborted":
|
|
562
591
|
with pytest.raises(LocalActivityExecutionAborted, match=message):
|
|
563
592
|
_execute_fixture(fixture)
|