durable-workflow 2.6.0rc3__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.0rc3/src/durable_workflow.egg-info → durable_workflow-2.6.2}/PKG-INFO +2 -1
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/README.md +1 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/pyproject.toml +3 -3
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/__init__.py +2 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/activity.py +28 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/client.py +18 -4
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/errors.py +20 -3
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/worker.py +34 -11
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/workflow.py +128 -15
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2/src/durable_workflow.egg-info}/PKG-INFO +2 -1
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/SOURCES.txt +3 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_errors.py +5 -3
- {durable_workflow-2.6.0rc3 → 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.0rc3 → 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.0rc3 → durable_workflow-2.6.2}/LICENSE +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/setup.cfg +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_client.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_invocable.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_metrics.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_queries.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_replay.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_replay_clock.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_schedules.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_serializer.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_signals.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_sleep.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_sync.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_updates.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_worker_observation.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/tests/test_workflow_updates_conformance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durable-workflow
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.2
|
|
4
4
|
Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
|
|
5
5
|
Author: Durable Workflow Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -139,6 +139,7 @@ for the complete cross-SDK contract.
|
|
|
139
139
|
- [Python SDK guide](https://durable-workflow.com/docs/2.0/polyglot/python/)
|
|
140
140
|
- [Complete SDK reference](docs/sdk-reference.md)
|
|
141
141
|
- [Runnable examples](examples/)
|
|
142
|
+
- [Separate workflow and activity worker processes](docs/guides/separate-workers.md)
|
|
142
143
|
- [Symmetric SDK playground](https://github.com/durable-workflow/sample-app#symmetric-sdk-playground)
|
|
143
144
|
|
|
144
145
|
## Runtime choices
|
|
@@ -99,6 +99,7 @@ for the complete cross-SDK contract.
|
|
|
99
99
|
- [Python SDK guide](https://durable-workflow.com/docs/2.0/polyglot/python/)
|
|
100
100
|
- [Complete SDK reference](docs/sdk-reference.md)
|
|
101
101
|
- [Runnable examples](examples/)
|
|
102
|
+
- [Separate workflow and activity worker processes](docs/guides/separate-workers.md)
|
|
102
103
|
- [Symmetric SDK playground](https://github.com/durable-workflow/sample-app#symmetric-sdk-playground)
|
|
103
104
|
|
|
104
105
|
## Runtime choices
|
|
@@ -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
|
|
|
@@ -3527,9 +3527,18 @@ class Client:
|
|
|
3527
3527
|
``workflow_type`` is the language-neutral type key registered via
|
|
3528
3528
|
:func:`durable_workflow.workflow.defn`. ``task_queue`` selects which
|
|
3529
3529
|
worker pool picks up the workflow. ``workflow_id`` is the
|
|
3530
|
-
caller-supplied instance
|
|
3531
|
-
|
|
3532
|
-
|
|
3530
|
+
caller-supplied instance ID. The PHP Server start contract accepts
|
|
3531
|
+
``duplicate_policy="fail"`` (also the default when omitted) or
|
|
3532
|
+
``"use-existing"``. ``fail`` raises :class:`WorkflowAlreadyStarted`
|
|
3533
|
+
if the ID already exists. ``use-existing`` returns the original run
|
|
3534
|
+
only while it is pending, running or waiting. Both policies reject
|
|
3535
|
+
a closed instance. Neither replaces a run or changes its input or
|
|
3536
|
+
deadlines. Use a new workflow ID for a new independent execution.
|
|
3537
|
+
|
|
3538
|
+
The SDK forwards the policy unchanged. ``reject``, ``allow`` and
|
|
3539
|
+
``terminate_existing`` are not supported wire values. A resource-keyed
|
|
3540
|
+
workflow ID coordinates start admission; it does not revoke work
|
|
3541
|
+
already running in an external system.
|
|
3533
3542
|
|
|
3534
3543
|
``input`` is a list of positional arguments passed to the workflow's
|
|
3535
3544
|
``run`` method; the SDK encodes the list with the default payload
|
|
@@ -5303,15 +5312,20 @@ class Client:
|
|
|
5303
5312
|
message: str,
|
|
5304
5313
|
failure_type: str | None = None,
|
|
5305
5314
|
stack_trace: str | None = None,
|
|
5315
|
+
failure_reason: str | None = None,
|
|
5306
5316
|
) -> Any:
|
|
5307
|
-
"""Report a workflow task failure
|
|
5317
|
+
"""Report a workflow task failure without closing the run.
|
|
5308
5318
|
|
|
5309
5319
|
Worker-plane endpoint. Task failures (e.g. non-determinism) are
|
|
5310
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.
|
|
5311
5323
|
"""
|
|
5312
5324
|
failure: dict[str, Any] = {"message": message}
|
|
5313
5325
|
if failure_type is not None:
|
|
5314
5326
|
failure["type"] = failure_type
|
|
5327
|
+
if failure_reason is not None:
|
|
5328
|
+
failure["reason"] = failure_reason
|
|
5315
5329
|
if stack_trace is not None:
|
|
5316
5330
|
failure["stack_trace"] = stack_trace
|
|
5317
5331
|
body: dict[str, Any] = {
|
|
@@ -312,8 +312,10 @@ class WorkflowNotFound(DurableWorkflowError):
|
|
|
312
312
|
class WorkflowAlreadyStarted(DurableWorkflowError):
|
|
313
313
|
"""A start request collided with an existing instance id.
|
|
314
314
|
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
The PHP Server raises this for an existing ``workflow_id`` when the
|
|
316
|
+
duplicate policy is ``fail`` (the default), or when ``use-existing``
|
|
317
|
+
addresses a closed instance. ``use-existing`` returns a handle only
|
|
318
|
+
while the original run is pending, running or waiting.
|
|
317
319
|
"""
|
|
318
320
|
|
|
319
321
|
def __init__(self, workflow_id: str) -> None:
|
|
@@ -463,6 +465,21 @@ class WorkflowPayloadDecodeError(DurableWorkflowError):
|
|
|
463
465
|
self.exception_type = exception_type
|
|
464
466
|
|
|
465
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
|
+
|
|
466
483
|
class NonDeterministicReplayError(DurableWorkflowError):
|
|
467
484
|
"""Current workflow code yielded a command shape that does not match history."""
|
|
468
485
|
|
|
@@ -874,7 +891,7 @@ def _raise_for_status(status: int, body: object, *, context: str = "") -> None:
|
|
|
874
891
|
if status == 409:
|
|
875
892
|
if reason == "schedule_already_exists":
|
|
876
893
|
raise ScheduleAlreadyExists(context)
|
|
877
|
-
if reason
|
|
894
|
+
if reason in ("duplicate_not_allowed", "instance_already_started"):
|
|
878
895
|
raise WorkflowAlreadyStarted(context)
|
|
879
896
|
if reason == "run_not_active":
|
|
880
897
|
if operation == "signal":
|
|
@@ -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:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durable-workflow
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.2
|
|
4
4
|
Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
|
|
5
5
|
Author: Durable Workflow Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -139,6 +139,7 @@ for the complete cross-SDK contract.
|
|
|
139
139
|
- [Python SDK guide](https://durable-workflow.com/docs/2.0/polyglot/python/)
|
|
140
140
|
- [Complete SDK reference](docs/sdk-reference.md)
|
|
141
141
|
- [Runnable examples](examples/)
|
|
142
|
+
- [Separate workflow and activity worker processes](docs/guides/separate-workers.md)
|
|
142
143
|
- [Symmetric SDK playground](https://github.com/durable-workflow/sample-app#symmetric-sdk-playground)
|
|
143
144
|
|
|
144
145
|
## Runtime choices
|
{durable_workflow-2.6.0rc3 → durable_workflow-2.6.2}/src/durable_workflow.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
@@ -57,9 +57,11 @@ class TestRaiseForStatus:
|
|
|
57
57
|
with pytest.raises(ServerError):
|
|
58
58
|
_raise_for_status(404, {"reason": "other"})
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
@pytest.mark.parametrize("reason", ["duplicate_not_allowed", "instance_already_started"])
|
|
61
|
+
def test_409_duplicate(self, reason: str) -> None:
|
|
62
|
+
with pytest.raises(WorkflowAlreadyStarted) as duplicate:
|
|
63
|
+
_raise_for_status(409, {"reason": reason}, context="resource-017")
|
|
64
|
+
assert duplicate.value.workflow_id == "resource-017"
|
|
63
65
|
|
|
64
66
|
def test_409_query_rejected(self) -> None:
|
|
65
67
|
with pytest.raises(QueryFailed):
|