durable-workflow 2.5.15__tar.gz → 2.6.0__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.15/src/durable_workflow.egg-info → durable_workflow-2.6.0}/PKG-INFO +9 -1
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/README.md +8 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/pyproject.toml +3 -3
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_cooperative_cancellation.py +9 -1
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/client.py +11 -9
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/worker.py +42 -123
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/workflow.py +164 -14
- {durable_workflow-2.5.15 → durable_workflow-2.6.0/src/durable_workflow.egg-info}/PKG-INFO +9 -1
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow.egg-info/SOURCES.txt +2 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_ci_checkout.py +39 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cooperative_cancellation_worker.py +138 -3
- durable_workflow-2.6.0/tests/test_nexus_durable_wait.py +241 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_nexus_namespace.py +12 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_replay_regression_corpus.py +27 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_runtime_external_payload_transport.py +3 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_worker.py +16 -57
- durable_workflow-2.6.0/tests/test_worker_observation.py +299 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/LICENSE +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/setup.cfg +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_client.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_errors.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_invocable.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_metrics.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_queries.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_replay.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_replay_clock.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_schedules.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_serializer.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_signals.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_sleep.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_sync.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_updates.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.5.15 → durable_workflow-2.6.0}/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.
|
|
3
|
+
Version: 2.6.0
|
|
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
|
|
@@ -40,6 +40,7 @@ Dynamic: license-file
|
|
|
40
40
|
|
|
41
41
|
# Durable Workflow Python SDK
|
|
42
42
|
|
|
43
|
+
[](https://sonarcloud.io/summary/new_code?id=durable-workflow_sdk-python)
|
|
43
44
|
[](https://github.com/durable-workflow/sdk-python/actions/workflows/ci.yml)
|
|
44
45
|
[](https://pypi.org/project/durable-workflow/)
|
|
45
46
|
[](https://pypi.org/project/durable-workflow/)
|
|
@@ -168,6 +169,13 @@ admission also needs `prepared_local_activities`, with
|
|
|
168
169
|
`prepared_local_activity_cancellation_policies` for explicit local policies.
|
|
169
170
|
Independently cancellable scopes remain disabled.
|
|
170
171
|
|
|
172
|
+
For synchronous Nexus completion waits, the cooperative worker also negotiates
|
|
173
|
+
`nexus_cooperative_cancellation` when Server advertises the installed
|
|
174
|
+
`workflow_run` binding. Owned target cleanup keeps the caller pending until the
|
|
175
|
+
Server commits delivery at the original call. Async and acceptance-only calls
|
|
176
|
+
keep their admission behavior. Existing terminal results and recorded legacy
|
|
177
|
+
Nexus histories retain their replay semantics.
|
|
178
|
+
|
|
171
179
|
The cooperative worker supervises async and synchronous activity callbacks
|
|
172
180
|
independently of application heartbeats. Existing terminal cancellation remains
|
|
173
181
|
available. See the [cancellation guide](docs/cooperative-cancellation-design.md)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Durable Workflow Python SDK
|
|
2
2
|
|
|
3
|
+
[](https://sonarcloud.io/summary/new_code?id=durable-workflow_sdk-python)
|
|
3
4
|
[](https://github.com/durable-workflow/sdk-python/actions/workflows/ci.yml)
|
|
4
5
|
[](https://pypi.org/project/durable-workflow/)
|
|
5
6
|
[](https://pypi.org/project/durable-workflow/)
|
|
@@ -128,6 +129,13 @@ admission also needs `prepared_local_activities`, with
|
|
|
128
129
|
`prepared_local_activity_cancellation_policies` for explicit local policies.
|
|
129
130
|
Independently cancellable scopes remain disabled.
|
|
130
131
|
|
|
132
|
+
For synchronous Nexus completion waits, the cooperative worker also negotiates
|
|
133
|
+
`nexus_cooperative_cancellation` when Server advertises the installed
|
|
134
|
+
`workflow_run` binding. Owned target cleanup keeps the caller pending until the
|
|
135
|
+
Server commits delivery at the original call. Async and acceptance-only calls
|
|
136
|
+
keep their admission behavior. Existing terminal results and recorded legacy
|
|
137
|
+
Nexus histories retain their replay semantics.
|
|
138
|
+
|
|
131
139
|
The cooperative worker supervises async and synchronous activity callbacks
|
|
132
140
|
independently of application heartbeats. Existing terminal cancellation remains
|
|
133
141
|
available. See the [cancellation guide](docs/cooperative-cancellation-design.md)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.6.0"
|
|
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.
|
|
75
|
-
registry-version = "2.
|
|
74
|
+
product-train = "2.6.0"
|
|
75
|
+
registry-version = "2.6.0"
|
|
76
76
|
supported-server-versions = "2.8.0"
|
|
77
77
|
worker-protocol-version = "1.20"
|
|
78
78
|
control-plane-version = "2"
|
{durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_cooperative_cancellation.py
RENAMED
|
@@ -16,7 +16,10 @@ from .errors import NonDeterministicReplayError
|
|
|
16
16
|
|
|
17
17
|
REQUEST_EVENT = "CooperativeCancellationRequested"
|
|
18
18
|
DELIVERY_EVENT = "CooperativeCancellationDelivered"
|
|
19
|
-
_CALL_KINDS = {
|
|
19
|
+
_CALL_KINDS = {
|
|
20
|
+
"activity", "local_activity", "timer", "condition", "signal", "child", "parallel", "selection_handle",
|
|
21
|
+
"service_operation",
|
|
22
|
+
}
|
|
20
23
|
_RESOLUTION_EVENTS = {
|
|
21
24
|
"ActivityCompleted",
|
|
22
25
|
"ActivityFailed",
|
|
@@ -31,6 +34,9 @@ _RESOLUTION_EVENTS = {
|
|
|
31
34
|
"ChildRunFailed",
|
|
32
35
|
"ChildRunCancelled",
|
|
33
36
|
"ChildRunTerminated",
|
|
37
|
+
"ServiceCallCompleted",
|
|
38
|
+
"ServiceCallFailed",
|
|
39
|
+
"ServiceCallCancelled",
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
|
|
@@ -265,6 +271,8 @@ def read_cancellation_history(
|
|
|
265
271
|
"ChildRunFailed",
|
|
266
272
|
"ChildRunCancelled",
|
|
267
273
|
"ChildRunTerminated",
|
|
274
|
+
"ServiceCallFailed",
|
|
275
|
+
"ServiceCallCancelled",
|
|
268
276
|
}:
|
|
269
277
|
failed.add(sequence)
|
|
270
278
|
state = CancellationHistory(
|
|
@@ -302,8 +302,8 @@ def _contains_inline_payload_envelope(value: object) -> bool:
|
|
|
302
302
|
if set(value) == {"codec", "blob"} and isinstance(value.get("blob"), str):
|
|
303
303
|
return True
|
|
304
304
|
if (
|
|
305
|
-
value.get("type") == "record_side_effect"
|
|
306
|
-
and isinstance(value.get("
|
|
305
|
+
(value.get("type") == "record_side_effect" and isinstance(value.get("result"), str))
|
|
306
|
+
or (value.get("type") == "start_service_operation" and isinstance(value.get("request_payload"), str))
|
|
307
307
|
):
|
|
308
308
|
return True
|
|
309
309
|
return any(_contains_inline_payload_envelope(item) for item in value.values())
|
|
@@ -2097,21 +2097,23 @@ class Client:
|
|
|
2097
2097
|
slot: tuple[str | int, ...] = (),
|
|
2098
2098
|
) -> Any:
|
|
2099
2099
|
if isinstance(value, dict):
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2100
|
+
command_type = value.get("type")
|
|
2101
|
+
blob_key = (
|
|
2102
|
+
{"record_side_effect": "result", "start_service_operation": "request_payload"}.get(command_type)
|
|
2103
|
+
if isinstance(command_type, str) else None
|
|
2104
|
+
)
|
|
2105
|
+
if blob_key is not None and isinstance(value.get(blob_key), str):
|
|
2104
2106
|
normalized_command = dict(value)
|
|
2105
2107
|
externalized_result = await self._externalize_runtime_payloads(
|
|
2106
|
-
{"codec": serializer.AVRO_CODEC, "blob": value[
|
|
2108
|
+
{"codec": value.get("payload_codec", serializer.AVRO_CODEC), "blob": value[blob_key]},
|
|
2107
2109
|
worker=worker,
|
|
2108
2110
|
transport=transport,
|
|
2109
2111
|
uploaded=uploaded,
|
|
2110
2112
|
completion=completion,
|
|
2111
|
-
slot=(*slot,
|
|
2113
|
+
slot=(*slot, blob_key),
|
|
2112
2114
|
)
|
|
2113
2115
|
if "external_payload" in externalized_result:
|
|
2114
|
-
normalized_command[
|
|
2116
|
+
normalized_command[blob_key] = externalized_result
|
|
2115
2117
|
value = normalized_command
|
|
2116
2118
|
|
|
2117
2119
|
if set(value) == {"codec", "blob"} and isinstance(value.get("blob"), str):
|
|
@@ -101,10 +101,8 @@ from .workflow import (
|
|
|
101
101
|
Command,
|
|
102
102
|
FailWorkflow,
|
|
103
103
|
LocalActivityExecutionAborted,
|
|
104
|
-
NexusServiceCall,
|
|
105
104
|
PreparedLocalActivityCall,
|
|
106
105
|
RecordLocalActivity,
|
|
107
|
-
RecordSideEffect,
|
|
108
106
|
ReplayOutcome,
|
|
109
107
|
ScheduleActivity,
|
|
110
108
|
StartChildWorkflow,
|
|
@@ -169,7 +167,6 @@ _WORKFLOW_TASK_COMPLETION_AMBIGUOUS_REJECTION_REASONS = {
|
|
|
169
167
|
}
|
|
170
168
|
_WORKFLOW_TASK_COMPLETION_MAX_ATTEMPTS = 3
|
|
171
169
|
_WORKFLOW_TASK_COMPLETION_RETRY_DELAYS = (0.05, 0.2)
|
|
172
|
-
_WORKFLOW_TASK_NEXUS_RESOLUTION_LIMIT = 100
|
|
173
170
|
_LOCAL_ACTIVITY_REPORT_LIMIT = 1000
|
|
174
171
|
_WORKER_WORKFLOW_FINGERPRINTS: dict[tuple[str, str], str] = {}
|
|
175
172
|
_P = ParamSpec("_P")
|
|
@@ -1072,6 +1069,7 @@ class Worker:
|
|
|
1072
1069
|
if "sticky_execution" in self.capabilities and not self._sticky_cache.enabled:
|
|
1073
1070
|
raise ValueError("sticky_execution requires a positive sticky_cache_capacity")
|
|
1074
1071
|
self._cooperative_cancellation_supported = False
|
|
1072
|
+
self._nexus_cooperative_cancellation_supported = False
|
|
1075
1073
|
self._prepared_local_activities_supported = False
|
|
1076
1074
|
self._prepared_local_activity_groups_supported = False
|
|
1077
1075
|
self._allow_cancellation_scope_authoring = False
|
|
@@ -1087,6 +1085,9 @@ class Worker:
|
|
|
1087
1085
|
raise ValueError("prepared local groups require prepared_local_activities capability")
|
|
1088
1086
|
if "prepared_local_activities" in self.capabilities and "cooperative_cancellation" not in self.capabilities:
|
|
1089
1087
|
raise ValueError("prepared local activities require cooperative_cancellation capability")
|
|
1088
|
+
if ("nexus_cooperative_cancellation" in self.capabilities
|
|
1089
|
+
and "cooperative_cancellation" not in self.capabilities):
|
|
1090
|
+
raise ValueError("Nexus cooperative cancellation requires cooperative_cancellation capability")
|
|
1090
1091
|
if any(not capability for capability in self.capabilities):
|
|
1091
1092
|
raise ValueError("worker capabilities must be non-empty strings")
|
|
1092
1093
|
self.worker_id = worker_id or f"py-worker-{uuid.uuid4().hex[:8]}"
|
|
@@ -1272,6 +1273,17 @@ class Worker:
|
|
|
1272
1273
|
)
|
|
1273
1274
|
if "cooperative_cancellation" in self.capabilities and not self._cooperative_cancellation_supported:
|
|
1274
1275
|
raise RuntimeError("cooperative cancellation requires explicit compatible runtime and worker protocol 1.20")
|
|
1276
|
+
self._nexus_cooperative_cancellation_supported = (
|
|
1277
|
+
self._cooperative_cancellation_supported
|
|
1278
|
+
and isinstance(server_capabilities, Mapping)
|
|
1279
|
+
and server_capabilities.get("nexus_cooperative_cancellation") is True
|
|
1280
|
+
and isinstance(server_capabilities.get("nexus_cooperative_cancellation_binding_kinds"), list)
|
|
1281
|
+
and "workflow_run" in server_capabilities["nexus_cooperative_cancellation_binding_kinds"]
|
|
1282
|
+
)
|
|
1283
|
+
if "nexus_cooperative_cancellation" in self.capabilities and not self._nexus_cooperative_cancellation_supported:
|
|
1284
|
+
raise RuntimeError(
|
|
1285
|
+
"nexus_cooperative_cancellation_not_supported: Server must advertise its installed workflow_run bridge",
|
|
1286
|
+
)
|
|
1275
1287
|
self._prepared_local_activities_supported = (
|
|
1276
1288
|
"prepared_local_activities" in self.capabilities and self._cooperative_cancellation_supported
|
|
1277
1289
|
and isinstance(server_capabilities, Mapping)
|
|
@@ -1342,6 +1354,8 @@ class Worker:
|
|
|
1342
1354
|
capabilities.append(WORKFLOW_UPDATES_CAPABILITY)
|
|
1343
1355
|
capabilities.append(MESSAGE_STREAMS_CAPABILITY)
|
|
1344
1356
|
capabilities.extend(self.capabilities)
|
|
1357
|
+
if self._nexus_cooperative_cancellation_supported and "nexus_cooperative_cancellation" not in capabilities:
|
|
1358
|
+
capabilities.append("nexus_cooperative_cancellation")
|
|
1345
1359
|
if (
|
|
1346
1360
|
self._local_activity_cancellation_policies
|
|
1347
1361
|
and "prepared_local_activity_cancellation_policies" not in capabilities
|
|
@@ -1373,6 +1387,11 @@ class Worker:
|
|
|
1373
1387
|
capabilities=capabilities,
|
|
1374
1388
|
capability_manifest={
|
|
1375
1389
|
**affinity_manifest,
|
|
1390
|
+
**({"nexus_cooperative_cancellation": {
|
|
1391
|
+
"supported": True, "minimum_protocol_version": "1.20",
|
|
1392
|
+
"implementation": "owned_workflow_run_canonical_delivery",
|
|
1393
|
+
"binding_kinds": ["workflow_run"],
|
|
1394
|
+
}} if self._nexus_cooperative_cancellation_supported else {}),
|
|
1376
1395
|
**({"prepared_local_activities": {
|
|
1377
1396
|
"supported": True, "minimum_protocol_version": "1.20",
|
|
1378
1397
|
"implementation": "durable_sequential_admission",
|
|
@@ -1452,86 +1471,6 @@ class Worker:
|
|
|
1452
1471
|
|
|
1453
1472
|
return await handler(context)
|
|
1454
1473
|
|
|
1455
|
-
async def _resolve_workflow_nexus_commands(
|
|
1456
|
-
self,
|
|
1457
|
-
workflow_cls: type,
|
|
1458
|
-
history: list[dict[str, Any]],
|
|
1459
|
-
start_input: list[Any],
|
|
1460
|
-
task: dict[str, Any],
|
|
1461
|
-
*,
|
|
1462
|
-
payload_codec: str | None,
|
|
1463
|
-
command_codec: str,
|
|
1464
|
-
initial_commands: list[Command],
|
|
1465
|
-
) -> list[Command]:
|
|
1466
|
-
events = list(history)
|
|
1467
|
-
commands: list[Command] = list(initial_commands)
|
|
1468
|
-
recorded_commands: list[RecordSideEffect] = []
|
|
1469
|
-
|
|
1470
|
-
for _ in range(_WORKFLOW_TASK_NEXUS_RESOLUTION_LIMIT):
|
|
1471
|
-
nexus_commands = [command for command in commands if isinstance(command, NexusServiceCall)]
|
|
1472
|
-
if not nexus_commands:
|
|
1473
|
-
return recorded_commands + commands
|
|
1474
|
-
if len(nexus_commands) != len(commands):
|
|
1475
|
-
raise RuntimeError("Nexus service calls must be yielded one at a time")
|
|
1476
|
-
|
|
1477
|
-
for command in nexus_commands:
|
|
1478
|
-
result = await self.client.execute_nexus_operation(
|
|
1479
|
-
command.endpoint_name,
|
|
1480
|
-
command.service_name,
|
|
1481
|
-
command.operation_name,
|
|
1482
|
-
command.arguments,
|
|
1483
|
-
mode=command.mode,
|
|
1484
|
-
wait_for=command.wait_for,
|
|
1485
|
-
wait_timeout_seconds=command.wait_timeout_seconds,
|
|
1486
|
-
idempotency_key=command.idempotency_key,
|
|
1487
|
-
payload_codec=command.payload_codec,
|
|
1488
|
-
caller_namespace=command.caller_namespace,
|
|
1489
|
-
target_namespace=command.target_namespace,
|
|
1490
|
-
caller_workflow_instance_id=_string_or_none(task.get("workflow_id")),
|
|
1491
|
-
caller_workflow_run_id=_string_or_none(task.get("run_id")),
|
|
1492
|
-
service_sdk_language=command.service_sdk_language,
|
|
1493
|
-
artifact_tuple=command.artifact_tuple,
|
|
1494
|
-
published_artifact_worker_execution=command.published_artifact_worker_execution,
|
|
1495
|
-
target_workflow_instance_id=command.target_workflow_instance_id,
|
|
1496
|
-
target_workflow_run_id=command.target_workflow_run_id,
|
|
1497
|
-
connection=command.connection,
|
|
1498
|
-
queue=command.queue,
|
|
1499
|
-
business_key=command.business_key,
|
|
1500
|
-
labels=command.labels,
|
|
1501
|
-
memo=command.memo,
|
|
1502
|
-
search_attributes=command.search_attributes,
|
|
1503
|
-
duplicate_start_policy=command.duplicate_start_policy,
|
|
1504
|
-
raise_on_failure=False,
|
|
1505
|
-
)
|
|
1506
|
-
recorded_payload = result.to_recorded_payload()
|
|
1507
|
-
recorded_commands.append(RecordSideEffect(recorded_payload))
|
|
1508
|
-
events.append({
|
|
1509
|
-
"event_type": "SideEffectRecorded",
|
|
1510
|
-
"payload": {
|
|
1511
|
-
"payload_codec": command_codec,
|
|
1512
|
-
"result": serializer.encode(recorded_payload, codec=command_codec),
|
|
1513
|
-
},
|
|
1514
|
-
})
|
|
1515
|
-
|
|
1516
|
-
outcome = replay(
|
|
1517
|
-
workflow_cls,
|
|
1518
|
-
events,
|
|
1519
|
-
start_input,
|
|
1520
|
-
workflow_id=task.get("workflow_id"),
|
|
1521
|
-
run_id=_string_or_none(task.get("run_id")) or "",
|
|
1522
|
-
workflow_command_id=(
|
|
1523
|
-
_string_or_none(task.get("workflow_command_id"))
|
|
1524
|
-
or _string_or_none(task.get("task_id"))
|
|
1525
|
-
),
|
|
1526
|
-
payload_codec=payload_codec,
|
|
1527
|
-
external_storage=self.external_storage,
|
|
1528
|
-
external_storage_cache=self.external_storage_cache,
|
|
1529
|
-
cancel_requested=bool(task.get("cancel_requested", False)),
|
|
1530
|
-
)
|
|
1531
|
-
commands = list(outcome.commands)
|
|
1532
|
-
|
|
1533
|
-
raise RuntimeError("workflow yielded too many consecutive Nexus service calls")
|
|
1534
|
-
|
|
1535
1474
|
async def _renew_workflow_payload_lease(self, task: dict[str, Any]) -> None:
|
|
1536
1475
|
if self._stop.is_set() or self._local_activity_shutdown.is_set():
|
|
1537
1476
|
raise LocalActivityExecutionAborted("worker shutdown stopped its workflow payload transfer")
|
|
@@ -1852,6 +1791,11 @@ class Worker:
|
|
|
1852
1791
|
# Earlier authored commands must commit on this claim first.
|
|
1853
1792
|
# Completion releases the claim; a successor replays their durable results.
|
|
1854
1793
|
return outcome, history
|
|
1794
|
+
if intent.call_kind == "service_operation" and not self._nexus_cooperative_cancellation_supported:
|
|
1795
|
+
raise LocalActivityExecutionAborted(
|
|
1796
|
+
"nexus_cooperative_cancellation_not_supported: "
|
|
1797
|
+
"delivery requires installed Server discovery and a cooperative protocol 1.20 worker",
|
|
1798
|
+
)
|
|
1855
1799
|
delivery_attempts += 1
|
|
1856
1800
|
if delivery_attempts > 3:
|
|
1857
1801
|
raise LocalActivityExecutionAborted("cancellation replay did not converge on its canonical delivery")
|
|
@@ -1866,7 +1810,20 @@ class Worker:
|
|
|
1866
1810
|
operation_sequence_span=intent.operation_sequence_span,
|
|
1867
1811
|
)
|
|
1868
1812
|
if delivery_reply.get("delivered") is False:
|
|
1869
|
-
|
|
1813
|
+
pending_reasons = {
|
|
1814
|
+
"service_operation": {"cancellation_waiting_for_service_operation"},
|
|
1815
|
+
"child": {"cancellation_waiting_for_child"},
|
|
1816
|
+
"activity": {"cancellation_waiting_for_activity"},
|
|
1817
|
+
"local_activity": {"cancellation_waiting_for_activity"},
|
|
1818
|
+
"parallel": {"cancellation_waiting_for_child", "cancellation_waiting_for_activity"},
|
|
1819
|
+
"selection_handle": {"cancellation_waiting_for_child", "cancellation_waiting_for_activity"},
|
|
1820
|
+
}.get(intent.call_kind, set())
|
|
1821
|
+
if (delivery_reply.get("reason") not in pending_reasons
|
|
1822
|
+
or delivery_reply.get("claim_released") is not True
|
|
1823
|
+
or delivery_reply.get("task_id") != task["task_id"]
|
|
1824
|
+
or delivery_reply.get("workflow_run_id") != task["run_id"]):
|
|
1825
|
+
raise LocalActivityExecutionAborted("pending cancellation lacks this claim's release receipt")
|
|
1826
|
+
raise _WorkflowClaimDeferred("pending cancellation: target cleanup continues on a new claim")
|
|
1870
1827
|
except _WorkflowClaimDeferred:
|
|
1871
1828
|
raise
|
|
1872
1829
|
except Exception as error:
|
|
@@ -2675,45 +2632,7 @@ class Worker:
|
|
|
2675
2632
|
log.warning("failed to report replay failure: %s", fe)
|
|
2676
2633
|
return None
|
|
2677
2634
|
|
|
2678
|
-
|
|
2679
|
-
workflow_commands = await self._resolve_workflow_nexus_commands(
|
|
2680
|
-
cls,
|
|
2681
|
-
list(history),
|
|
2682
|
-
start_input,
|
|
2683
|
-
task,
|
|
2684
|
-
payload_codec=codec,
|
|
2685
|
-
command_codec=command_codec,
|
|
2686
|
-
initial_commands=list(outcome.commands),
|
|
2687
|
-
)
|
|
2688
|
-
except Exception as e:
|
|
2689
|
-
log.exception("workflow Nexus service call resolution failed")
|
|
2690
|
-
last_local = max(
|
|
2691
|
-
(index for index, command in enumerate(outcome.commands)
|
|
2692
|
-
if isinstance(command, RecordLocalActivity) and command.outcome is not None),
|
|
2693
|
-
default=-1,
|
|
2694
|
-
)
|
|
2695
|
-
if last_local >= 0:
|
|
2696
|
-
workflow_commands = [
|
|
2697
|
-
*outcome.commands[:last_local + 1],
|
|
2698
|
-
FailWorkflow(
|
|
2699
|
-
f"Nexus resolution after local activity failed: {e}",
|
|
2700
|
-
exception_type=type(e).__name__,
|
|
2701
|
-
non_retryable=True,
|
|
2702
|
-
),
|
|
2703
|
-
]
|
|
2704
|
-
else:
|
|
2705
|
-
try:
|
|
2706
|
-
await self.client.fail_workflow_task(
|
|
2707
|
-
task_id=task_id,
|
|
2708
|
-
lease_owner=self.worker_id,
|
|
2709
|
-
workflow_task_attempt=attempt,
|
|
2710
|
-
message=f"workflow Nexus service call resolution failed: {e}",
|
|
2711
|
-
failure_type=type(e).__name__,
|
|
2712
|
-
stack_trace=traceback.format_exc(),
|
|
2713
|
-
)
|
|
2714
|
-
except Exception as fe:
|
|
2715
|
-
log.warning("failed to report workflow Nexus resolution failure: %s", fe)
|
|
2716
|
-
return None
|
|
2635
|
+
workflow_commands = list(outcome.commands)
|
|
2717
2636
|
|
|
2718
2637
|
if (
|
|
2719
2638
|
any(isinstance(command, UpsertMemo) for command in workflow_commands)
|