durable-workflow 2.5.5__tar.gz → 2.5.7__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.5/src/durable_workflow.egg-info → durable_workflow-2.5.7}/PKG-INFO +11 -9
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/README.md +10 -8
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/pyproject.toml +4 -4
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/client.py +2 -1
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/worker.py +25 -10
- {durable_workflow-2.5.5 → durable_workflow-2.5.7/src/durable_workflow.egg-info}/PKG-INFO +11 -9
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_scope_opening.py +2 -2
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_client.py +6 -1
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cooperative_cancellation_client.py +4 -7
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cooperative_cancellation_worker.py +1 -1
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cooperative_remote_worker.py +3 -3
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_client.py +2 -2
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_release_metadata.py +2 -2
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_replay_regression_corpus.py +42 -1
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_worker.py +88 -1
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/LICENSE +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/setup.cfg +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/workflow.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_errors.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_invocable.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_metrics.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_queries.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_replay.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_schedules.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_serializer.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_signals.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_sleep.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_sync.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_updates.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.5.5 → durable_workflow-2.5.7}/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.5.
|
|
3
|
+
Version: 2.5.7
|
|
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
|
|
@@ -155,12 +155,15 @@ capabilities with Server at startup. Use stable `2.x` SDK and Server channels
|
|
|
155
155
|
for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
|
|
156
156
|
documents protocol and upgrade guarantees.
|
|
157
157
|
|
|
158
|
-
##
|
|
158
|
+
## Worker protocol and cooperative cancellation
|
|
159
|
+
|
|
160
|
+
Workers use protocol 1.20 by default. For a Server that supports only 1.19, set
|
|
161
|
+
`DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.19`. This changes the request protocol
|
|
162
|
+
without enabling optional worker features.
|
|
159
163
|
|
|
160
164
|
Use `request_cancellation()` for bounded, replayable workflow cleanup. Opt in
|
|
161
165
|
against a Server that advertises protocol 1.20 and the required capabilities:
|
|
162
|
-
|
|
163
|
-
`cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
|
|
166
|
+
include `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
|
|
164
167
|
admission also needs `prepared_local_activities`, with
|
|
165
168
|
`prepared_local_activity_cancellation_policies` for explicit local policies.
|
|
166
169
|
Independently cancellable scopes remain disabled.
|
|
@@ -196,11 +199,10 @@ to true. CI verifies that checkout, builds the candidate Server, enables protoco
|
|
|
196
199
|
authority and exact source provenance. An optional exact `native_commit` mounts
|
|
197
200
|
that public Native checkout read-only into the test stack. The image's published
|
|
198
201
|
Composer authority stays intact and the evidence identifies the source overlay.
|
|
199
|
-
These are source qualification runs. For a local
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
keeps protocol 1.19 and skips this unpublished feature's connected cases.
|
|
202
|
+
These are source qualification runs. For a local candidate, set
|
|
203
|
+
`DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases fail if the
|
|
204
|
+
runtime does not discover the required capability. Ordinary CI uses the SDK's
|
|
205
|
+
default protocol and skips the optional connected cooperative cases.
|
|
204
206
|
|
|
205
207
|
## License
|
|
206
208
|
|
|
@@ -115,12 +115,15 @@ capabilities with Server at startup. Use stable `2.x` SDK and Server channels
|
|
|
115
115
|
for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
|
|
116
116
|
documents protocol and upgrade guarantees.
|
|
117
117
|
|
|
118
|
-
##
|
|
118
|
+
## Worker protocol and cooperative cancellation
|
|
119
|
+
|
|
120
|
+
Workers use protocol 1.20 by default. For a Server that supports only 1.19, set
|
|
121
|
+
`DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.19`. This changes the request protocol
|
|
122
|
+
without enabling optional worker features.
|
|
119
123
|
|
|
120
124
|
Use `request_cancellation()` for bounded, replayable workflow cleanup. Opt in
|
|
121
125
|
against a Server that advertises protocol 1.20 and the required capabilities:
|
|
122
|
-
|
|
123
|
-
`cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
|
|
126
|
+
include `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
|
|
124
127
|
admission also needs `prepared_local_activities`, with
|
|
125
128
|
`prepared_local_activity_cancellation_policies` for explicit local policies.
|
|
126
129
|
Independently cancellable scopes remain disabled.
|
|
@@ -156,11 +159,10 @@ to true. CI verifies that checkout, builds the candidate Server, enables protoco
|
|
|
156
159
|
authority and exact source provenance. An optional exact `native_commit` mounts
|
|
157
160
|
that public Native checkout read-only into the test stack. The image's published
|
|
158
161
|
Composer authority stays intact and the evidence identifies the source overlay.
|
|
159
|
-
These are source qualification runs. For a local
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
keeps protocol 1.19 and skips this unpublished feature's connected cases.
|
|
162
|
+
These are source qualification runs. For a local candidate, set
|
|
163
|
+
`DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases fail if the
|
|
164
|
+
runtime does not discover the required capability. Ordinary CI uses the SDK's
|
|
165
|
+
default protocol and skips the optional connected cooperative cases.
|
|
164
166
|
|
|
165
167
|
## License
|
|
166
168
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "2.5.
|
|
7
|
+
version = "2.5.7"
|
|
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,10 +71,10 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
71
71
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
72
72
|
|
|
73
73
|
[tool.durable-workflow]
|
|
74
|
-
product-train = "2.5.
|
|
75
|
-
registry-version = "2.5.
|
|
74
|
+
product-train = "2.5.7"
|
|
75
|
+
registry-version = "2.5.7"
|
|
76
76
|
supported-server-versions = "2.5.16"
|
|
77
|
-
worker-protocol-version = "1.
|
|
77
|
+
worker-protocol-version = "1.20"
|
|
78
78
|
control-plane-version = "2"
|
|
79
79
|
durable-selection = true
|
|
80
80
|
durable-selection-minimum-worker-protocol-version = "1.19"
|
|
@@ -72,7 +72,8 @@ from .nexus import NexusOperationResult, nexus_request_payload
|
|
|
72
72
|
from .retry_policy import TransportRetryPolicy
|
|
73
73
|
from .worker_session import WorkerSessionOptions
|
|
74
74
|
|
|
75
|
-
PROTOCOL_VERSION = "1.
|
|
75
|
+
PROTOCOL_VERSION = "1.20"
|
|
76
|
+
MINIMUM_WORKER_PROTOCOL_VERSION = "1.19"
|
|
76
77
|
CONTROL_PLANE_VERSION = "2"
|
|
77
78
|
PORTABLE_WORKER_AFFINITY_CAPABILITY_MANIFEST: dict[str, dict[str, str | bool]] = {
|
|
78
79
|
"local_activities": {
|
|
@@ -55,6 +55,7 @@ from .client import (
|
|
|
55
55
|
CONTROL_PLANE_REQUEST_CONTRACT_VERSION,
|
|
56
56
|
CONTROL_PLANE_VERSION,
|
|
57
57
|
DEFAULT_SDK_VERSION,
|
|
58
|
+
MINIMUM_WORKER_PROTOCOL_VERSION,
|
|
58
59
|
PORTABLE_WORKER_AFFINITY_CAPABILITY_MANIFEST,
|
|
59
60
|
PROTOCOL_VERSION,
|
|
60
61
|
Client,
|
|
@@ -877,6 +878,16 @@ def _server_protocol_compatible(server_version: str, sdk_version: str) -> bool:
|
|
|
877
878
|
|
|
878
879
|
|
|
879
880
|
def _validate_server_compatibility(info: dict[str, Any]) -> None:
|
|
881
|
+
requested_protocol = _protocol_version_from_env(
|
|
882
|
+
"DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", PROTOCOL_VERSION,
|
|
883
|
+
)
|
|
884
|
+
if requested_protocol not in (MINIMUM_WORKER_PROTOCOL_VERSION, PROTOCOL_VERSION):
|
|
885
|
+
raise RuntimeError(
|
|
886
|
+
"Server compatibility error: sdk-python supports worker protocol "
|
|
887
|
+
f"{MINIMUM_WORKER_PROTOCOL_VERSION} or protocol {PROTOCOL_VERSION}; "
|
|
888
|
+
f"requested {requested_protocol!r}."
|
|
889
|
+
)
|
|
890
|
+
|
|
880
891
|
control_plane = info.get("control_plane")
|
|
881
892
|
if not isinstance(control_plane, dict):
|
|
882
893
|
raise RuntimeError(
|
|
@@ -914,15 +925,15 @@ def _validate_server_compatibility(info: dict[str, Any]) -> None:
|
|
|
914
925
|
if not isinstance(worker_protocol, dict):
|
|
915
926
|
raise RuntimeError(
|
|
916
927
|
"Server compatibility error: missing worker_protocol manifest; "
|
|
917
|
-
f"sdk-python requires worker_protocol.version {
|
|
928
|
+
f"sdk-python requires worker_protocol.version {requested_protocol}."
|
|
918
929
|
)
|
|
919
930
|
|
|
920
931
|
worker_protocol_version = _manifest_version(worker_protocol)
|
|
921
|
-
if not _server_protocol_compatible(worker_protocol_version,
|
|
932
|
+
if not _server_protocol_compatible(worker_protocol_version, requested_protocol):
|
|
922
933
|
raise RuntimeError(
|
|
923
934
|
"Server compatibility error: incompatible worker_protocol.version "
|
|
924
935
|
f"{worker_protocol_version!r}; sdk-python requires major-equal and "
|
|
925
|
-
f"minor>={
|
|
936
|
+
f"minor>={requested_protocol!r}."
|
|
926
937
|
)
|
|
927
938
|
|
|
928
939
|
auth_composition = info.get("auth_composition_contract")
|
|
@@ -4077,11 +4088,12 @@ class Worker:
|
|
|
4077
4088
|
try:
|
|
4078
4089
|
if self._stop.is_set():
|
|
4079
4090
|
return
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4091
|
+
loops = [asyncio.create_task(self._heartbeat_loop())]
|
|
4092
|
+
if self.workflows:
|
|
4093
|
+
loops.append(asyncio.create_task(self._poll_workflow_tasks()))
|
|
4094
|
+
if self.activities:
|
|
4095
|
+
loops.append(asyncio.create_task(self._poll_activity_tasks()))
|
|
4096
|
+
if self.workflows and self._query_tasks_supported:
|
|
4085
4097
|
if self._can_clone_client_for_query_tasks():
|
|
4086
4098
|
self._start_query_task_thread()
|
|
4087
4099
|
else:
|
|
@@ -4243,7 +4255,7 @@ class Worker:
|
|
|
4243
4255
|
if self._stop.is_set():
|
|
4244
4256
|
raise asyncio.CancelledError
|
|
4245
4257
|
background_tasks.append(asyncio.create_task(self._heartbeat_loop()))
|
|
4246
|
-
if self._query_tasks_supported:
|
|
4258
|
+
if self.workflows and self._query_tasks_supported:
|
|
4247
4259
|
if self._can_clone_client_for_query_tasks():
|
|
4248
4260
|
self._start_query_task_thread()
|
|
4249
4261
|
else:
|
|
@@ -4295,7 +4307,7 @@ class Worker:
|
|
|
4295
4307
|
f"workflow {workflow_id} not terminal after {timeout}s (status={desc.status})"
|
|
4296
4308
|
)
|
|
4297
4309
|
|
|
4298
|
-
if next_task_kind == "workflow":
|
|
4310
|
+
if self.workflows and (next_task_kind == "workflow" or not self.activities):
|
|
4299
4311
|
await self._reserve_workflow_capacity()
|
|
4300
4312
|
try:
|
|
4301
4313
|
poll_start = time.perf_counter()
|
|
@@ -4347,6 +4359,9 @@ class Worker:
|
|
|
4347
4359
|
next_task_kind = "workflow"
|
|
4348
4360
|
continue
|
|
4349
4361
|
|
|
4362
|
+
if not self.activities:
|
|
4363
|
+
await asyncio.sleep(poll_interval)
|
|
4364
|
+
continue
|
|
4350
4365
|
self._validate_cooperative_activity_handlers()
|
|
4351
4366
|
if len(self._remote_activity_processes) >= self.max_concurrent_activity_tasks:
|
|
4352
4367
|
next_task_kind = "workflow"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durable-workflow
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.7
|
|
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
|
|
@@ -155,12 +155,15 @@ capabilities with Server at startup. Use stable `2.x` SDK and Server channels
|
|
|
155
155
|
for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
|
|
156
156
|
documents protocol and upgrade guarantees.
|
|
157
157
|
|
|
158
|
-
##
|
|
158
|
+
## Worker protocol and cooperative cancellation
|
|
159
|
+
|
|
160
|
+
Workers use protocol 1.20 by default. For a Server that supports only 1.19, set
|
|
161
|
+
`DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.19`. This changes the request protocol
|
|
162
|
+
without enabling optional worker features.
|
|
159
163
|
|
|
160
164
|
Use `request_cancellation()` for bounded, replayable workflow cleanup. Opt in
|
|
161
165
|
against a Server that advertises protocol 1.20 and the required capabilities:
|
|
162
|
-
|
|
163
|
-
`cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
|
|
166
|
+
include `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
|
|
164
167
|
admission also needs `prepared_local_activities`, with
|
|
165
168
|
`prepared_local_activity_cancellation_policies` for explicit local policies.
|
|
166
169
|
Independently cancellable scopes remain disabled.
|
|
@@ -196,11 +199,10 @@ to true. CI verifies that checkout, builds the candidate Server, enables protoco
|
|
|
196
199
|
authority and exact source provenance. An optional exact `native_commit` mounts
|
|
197
200
|
that public Native checkout read-only into the test stack. The image's published
|
|
198
201
|
Composer authority stays intact and the evidence identifies the source overlay.
|
|
199
|
-
These are source qualification runs. For a local
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
keeps protocol 1.19 and skips this unpublished feature's connected cases.
|
|
202
|
+
These are source qualification runs. For a local candidate, set
|
|
203
|
+
`DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases fail if the
|
|
204
|
+
runtime does not discover the required capability. Ordinary CI uses the SDK's
|
|
205
|
+
default protocol and skips the optional connected cooperative cases.
|
|
204
206
|
|
|
205
207
|
## License
|
|
206
208
|
|
|
@@ -152,8 +152,8 @@ async def test_invalid_authority_fails_before_io(change: dict[str, Any]) -> None
|
|
|
152
152
|
send.assert_not_awaited()
|
|
153
153
|
|
|
154
154
|
|
|
155
|
-
async def
|
|
156
|
-
monkeypatch.
|
|
155
|
+
async def test_previous_protocol_refuses_opening_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
156
|
+
monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
|
|
157
157
|
async with Client("http://server", namespace="tenant") as client:
|
|
158
158
|
with patch.object(client, "_request", new_callable=AsyncMock) as send:
|
|
159
159
|
with pytest.raises(ValueError, match="1.20"):
|
|
@@ -164,10 +164,15 @@ class TestHeaders:
|
|
|
164
164
|
|
|
165
165
|
def test_worker_headers(self, client: Client) -> None:
|
|
166
166
|
h = client._headers(worker=True)
|
|
167
|
-
assert PROTOCOL_VERSION == "1.
|
|
167
|
+
assert PROTOCOL_VERSION == "1.20"
|
|
168
168
|
assert h["X-Durable-Workflow-Protocol-Version"] == PROTOCOL_VERSION
|
|
169
169
|
assert "X-Durable-Workflow-Control-Plane-Version" not in h
|
|
170
170
|
|
|
171
|
+
def test_explicit_previous_worker_protocol(self, client: Client, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
172
|
+
monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
|
|
173
|
+
assert client._headers(worker=True)["X-Durable-Workflow-Protocol-Version"] == "1.19"
|
|
174
|
+
assert client._headers(worker=False)["X-Durable-Workflow-Control-Plane-Version"] == "2"
|
|
175
|
+
|
|
171
176
|
def test_conformance_protocol_header_overrides(self, client: Client, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
172
177
|
monkeypatch.setenv("DURABLE_WORKFLOW_CONTROL_PLANE_VERSION", "3")
|
|
173
178
|
monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "2.0")
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cooperative_cancellation_client.py
RENAMED
|
@@ -406,14 +406,11 @@ async def test_activity_stop_receipt_rejects_invalid_identity_before_io(
|
|
|
406
406
|
|
|
407
407
|
|
|
408
408
|
@pytest.mark.asyncio
|
|
409
|
-
@pytest.mark.parametrize("version", [
|
|
410
|
-
async def
|
|
411
|
-
client: Client, monkeypatch: pytest.MonkeyPatch, version: str
|
|
409
|
+
@pytest.mark.parametrize("version", ["1.19", "2.20", "invalid"])
|
|
410
|
+
async def test_activity_stop_receipt_requires_compatible_protocol(
|
|
411
|
+
client: Client, monkeypatch: pytest.MonkeyPatch, version: str,
|
|
412
412
|
) -> None:
|
|
413
|
-
|
|
414
|
-
monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
|
|
415
|
-
else:
|
|
416
|
-
monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", version)
|
|
413
|
+
monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", version)
|
|
417
414
|
with patch.object(client._http, "request", new_callable=AsyncMock) as send, pytest.raises(ValueError):
|
|
418
415
|
await client.acknowledge_activity_cancellation(
|
|
419
416
|
task_id="task/1", activity_attempt_id="attempt-1", lease_owner="worker-1", request_id="original-request",
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cooperative_cancellation_worker.py
RENAMED
|
@@ -134,7 +134,7 @@ class ClaimServer:
|
|
|
134
134
|
return {"outcome": "completed"}
|
|
135
135
|
|
|
136
136
|
async def worker(self, monkeypatch: pytest.MonkeyPatch, **kwargs: Any) -> Worker:
|
|
137
|
-
monkeypatch.
|
|
137
|
+
monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
|
|
138
138
|
# These transport/replay fixtures use local callbacks that share mock
|
|
139
139
|
# state with this process. They never poll or advertise remote handlers.
|
|
140
140
|
local_callbacks = kwargs.pop("activities", [])
|
|
@@ -94,7 +94,7 @@ async def application_failure(kind: str) -> None:
|
|
|
94
94
|
|
|
95
95
|
@pytest_asyncio.fixture
|
|
96
96
|
async def owner(monkeypatch: pytest.MonkeyPatch): # type: ignore[no-untyped-def]
|
|
97
|
-
monkeypatch.
|
|
97
|
+
monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
|
|
98
98
|
client = AsyncMock(spec=Client)
|
|
99
99
|
client.get_cluster_info.return_value = compatible_cluster_info(worker_protocol={
|
|
100
100
|
"version": "1.20", "server_capabilities": {"cooperative_cancellation": True},
|
|
@@ -317,8 +317,8 @@ async def test_client_observation_keeps_worker_credentials_namespace_and_fence(m
|
|
|
317
317
|
assert send.await_args.kwargs["json"] == {"activity_attempt_id": "attempt", "lease_owner": "owner"}
|
|
318
318
|
|
|
319
319
|
|
|
320
|
-
async def
|
|
321
|
-
monkeypatch.
|
|
320
|
+
async def test_client_observation_refuses_previous_protocol_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
321
|
+
monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
|
|
322
322
|
async with Client("http://runtime.test") as client:
|
|
323
323
|
with (patch.object(client._http, "request", new_callable=AsyncMock) as send,
|
|
324
324
|
pytest.raises(ValueError, match="1.20")):
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_client.py
RENAMED
|
@@ -66,8 +66,8 @@ async def test_invalid_claim_or_authority_never_starts_transport(change: dict[st
|
|
|
66
66
|
send.assert_not_awaited()
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
async def
|
|
70
|
-
monkeypatch.
|
|
69
|
+
async def test_previous_protocol_refuses_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
70
|
+
monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
|
|
71
71
|
async with Client("http://server") as client:
|
|
72
72
|
with patch.object(client, "_request", new_callable=AsyncMock) as send:
|
|
73
73
|
with pytest.raises(ValueError, match="1.20"):
|
|
@@ -45,13 +45,13 @@ def test_worker_release_identity_matches_supported_server_and_protocol() -> None
|
|
|
45
45
|
assert release["product-train"] == project["version"]
|
|
46
46
|
assert release["registry-version"] == project["version"].replace("-rc.", "rc")
|
|
47
47
|
assert check_release_metadata.VERSION_PATTERN.fullmatch(release["supported-server-versions"]) is not None
|
|
48
|
-
assert release["worker-protocol-version"] == PROTOCOL_VERSION == "1.
|
|
48
|
+
assert release["worker-protocol-version"] == PROTOCOL_VERSION == "1.20"
|
|
49
49
|
assert release["durable-selection"] is True
|
|
50
50
|
assert release["durable-selection-minimum-worker-protocol-version"] == "1.19"
|
|
51
51
|
|
|
52
52
|
assert declared_runtime_protocol_version(
|
|
53
53
|
(REPO_ROOT / "src" / "durable_workflow" / "client.py").read_text(encoding="utf-8")
|
|
54
|
-
) == "1.
|
|
54
|
+
) == "1.20"
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def pypi_json(source: SourceMetadata, **overrides: object) -> bytes:
|
|
@@ -9,7 +9,7 @@ from unittest.mock import AsyncMock
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
11
|
from durable_workflow import Replayer, Worker, serializer, workflow
|
|
12
|
-
from durable_workflow.client import Client, WorkflowStreamAppendItem
|
|
12
|
+
from durable_workflow.client import PROTOCOL_VERSION, Client, WorkflowExecution, WorkflowStreamAppendItem
|
|
13
13
|
from durable_workflow.errors import NonDeterministicReplayError, WorkflowCancelled, WorkflowPayloadDecodeError
|
|
14
14
|
from durable_workflow.workflow import (
|
|
15
15
|
LocalActivityExecutionAborted,
|
|
@@ -31,6 +31,7 @@ from tests.test_update_signal_condition_replay import (
|
|
|
31
31
|
PostConditionReceiversWorkflow,
|
|
32
32
|
UpdateSignalConditionTimerWorkflow,
|
|
33
33
|
)
|
|
34
|
+
from tests.test_worker import compatible_cluster_info
|
|
34
35
|
|
|
35
36
|
FIXTURE_SCHEMA = "durable-workflow.replay-regression/v1"
|
|
36
37
|
FIXTURE_DIR = Path(__file__).parent / "fixtures" / "replay_regressions"
|
|
@@ -489,6 +490,46 @@ def test_checked_in_replay_regression_corpus_uses_official_replayer(
|
|
|
489
490
|
_execute_fixture(fixture)
|
|
490
491
|
|
|
491
492
|
|
|
493
|
+
@pytest.mark.asyncio
|
|
494
|
+
async def test_workflow_only_driver_resumes_external_activity_without_activity_polling() -> None:
|
|
495
|
+
fixture = json.loads(
|
|
496
|
+
(FIXTURE_DIR / "workflow-only-worker-resumes-external-activity.json").read_text(encoding="utf-8")
|
|
497
|
+
)
|
|
498
|
+
client = AsyncMock(spec=Client)
|
|
499
|
+
client.get_cluster_info.return_value = compatible_cluster_info(worker_protocol={"version": PROTOCOL_VERSION})
|
|
500
|
+
client.register_worker.return_value = {"registered": True}
|
|
501
|
+
client.deregister_worker_registration.return_value = {"outcome": "deregistered"}
|
|
502
|
+
client.poll_activity_task.side_effect = AssertionError("workflow-only worker must not poll activities")
|
|
503
|
+
client.poll_workflow_task.side_effect = [None, {
|
|
504
|
+
"task_id": "external-activity-resume-task", "workflow_id": "external-activity-caller",
|
|
505
|
+
"run_id": "original-run", "workflow_type": fixture["workflow"]["type"],
|
|
506
|
+
"workflow_task_attempt": 1, "history_events": fixture["history"],
|
|
507
|
+
"arguments": serializer.encode(fixture["workflow"]["input"], codec="avro"),
|
|
508
|
+
"payload_codec": "avro",
|
|
509
|
+
}]
|
|
510
|
+
client.complete_workflow_task.return_value = {"outcome": "completed"}
|
|
511
|
+
|
|
512
|
+
async def describe(workflow_id: str) -> WorkflowExecution:
|
|
513
|
+
return WorkflowExecution(
|
|
514
|
+
workflow_id=workflow_id, run_id="original-run", workflow_type=fixture["workflow"]["type"],
|
|
515
|
+
status="completed" if client.complete_workflow_task.await_count else "running",
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
client.describe_workflow.side_effect = describe
|
|
519
|
+
worker = Worker(client, task_queue="external-activity", workflows=[GoldenSingleActivityWorkflow], activities=[])
|
|
520
|
+
|
|
521
|
+
result = await worker.run_until(workflow_id="external-activity-caller", timeout=1, poll_interval=0.01)
|
|
522
|
+
|
|
523
|
+
assert result.status == "completed"
|
|
524
|
+
assert result.run_id == "original-run"
|
|
525
|
+
assert client.poll_workflow_task.await_count == 2
|
|
526
|
+
client.poll_activity_task.assert_not_awaited()
|
|
527
|
+
client.complete_workflow_task.assert_awaited_once()
|
|
528
|
+
commands = client.complete_workflow_task.call_args.kwargs["commands"]
|
|
529
|
+
assert [command["type"] for command in commands] == [fixture["expected"]["type"]]
|
|
530
|
+
assert serializer.decode_envelope(commands[0]["result"]) == fixture["expected"]["result"]
|
|
531
|
+
|
|
532
|
+
|
|
492
533
|
@pytest.mark.asyncio
|
|
493
534
|
async def test_worker_replays_recorded_side_effects_across_history_pages() -> None:
|
|
494
535
|
fixture = json.loads((FIXTURE_DIR / "paged-recorded-side-effects.json").read_text(encoding="utf-8"))
|
|
@@ -1114,6 +1114,39 @@ class TestWorkerRegistration:
|
|
|
1114
1114
|
await worker._register()
|
|
1115
1115
|
mock_client.register_worker.assert_not_called()
|
|
1116
1116
|
|
|
1117
|
+
@pytest.mark.asyncio
|
|
1118
|
+
async def test_register_accepts_explicit_previous_protocol(
|
|
1119
|
+
self, mock_client: AsyncMock, monkeypatch: pytest.MonkeyPatch,
|
|
1120
|
+
) -> None:
|
|
1121
|
+
monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
|
|
1122
|
+
mock_client.get_cluster_info = AsyncMock(
|
|
1123
|
+
return_value=compatible_cluster_info(worker_protocol={"version": "1.19"})
|
|
1124
|
+
)
|
|
1125
|
+
worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
|
|
1126
|
+
await worker._register()
|
|
1127
|
+
mock_client.register_worker.assert_called_once()
|
|
1128
|
+
|
|
1129
|
+
@pytest.mark.asyncio
|
|
1130
|
+
async def test_default_protocol_refuses_previous_server_before_registration(self, mock_client: AsyncMock) -> None:
|
|
1131
|
+
mock_client.get_cluster_info = AsyncMock(
|
|
1132
|
+
return_value=compatible_cluster_info(worker_protocol={"version": "1.19"})
|
|
1133
|
+
)
|
|
1134
|
+
worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
|
|
1135
|
+
with pytest.raises(RuntimeError, match="minor>='1.20'"):
|
|
1136
|
+
await worker._register()
|
|
1137
|
+
mock_client.register_worker.assert_not_called()
|
|
1138
|
+
|
|
1139
|
+
@pytest.mark.asyncio
|
|
1140
|
+
@pytest.mark.parametrize("version", ["1.18", "1.21", "2.0"])
|
|
1141
|
+
async def test_register_rejects_unsupported_protocol_override(
|
|
1142
|
+
self, mock_client: AsyncMock, monkeypatch: pytest.MonkeyPatch, version: str,
|
|
1143
|
+
) -> None:
|
|
1144
|
+
monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", version)
|
|
1145
|
+
worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
|
|
1146
|
+
with pytest.raises(RuntimeError, match="supports worker protocol 1.19 or protocol 1.20"):
|
|
1147
|
+
await worker._register()
|
|
1148
|
+
mock_client.register_worker.assert_not_called()
|
|
1149
|
+
|
|
1117
1150
|
@pytest.mark.asyncio
|
|
1118
1151
|
async def test_register_accepts_higher_compatible_minor_protocol(self, mock_client: AsyncMock) -> None:
|
|
1119
1152
|
# Server is one minor ahead of the SDK. MINOR bumps in workflow:v2's
|
|
@@ -4403,6 +4436,8 @@ class TestWorkerShutdown:
|
|
|
4403
4436
|
client,
|
|
4404
4437
|
task_queue="orders",
|
|
4405
4438
|
worker_id="worker-only",
|
|
4439
|
+
workflows=[TestWorkflow],
|
|
4440
|
+
activities=[echo_activity],
|
|
4406
4441
|
poll_timeout=0.01,
|
|
4407
4442
|
heartbeat_interval=0.01,
|
|
4408
4443
|
)
|
|
@@ -4597,7 +4632,7 @@ class TestWorkerShutdown:
|
|
|
4597
4632
|
},
|
|
4598
4633
|
)
|
|
4599
4634
|
mock_client.deregister_worker_registration.side_effect = cleanup_error
|
|
4600
|
-
worker = Worker(mock_client, task_queue="q1")
|
|
4635
|
+
worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow])
|
|
4601
4636
|
worker._poll_workflow_tasks = AsyncMock( # type: ignore[method-assign]
|
|
4602
4637
|
side_effect=RuntimeError("worker loop failed")
|
|
4603
4638
|
)
|
|
@@ -4729,6 +4764,31 @@ class TestWorkerShutdown:
|
|
|
4729
4764
|
|
|
4730
4765
|
|
|
4731
4766
|
class TestPollLoops:
|
|
4767
|
+
@pytest.mark.asyncio
|
|
4768
|
+
@pytest.mark.parametrize("has_workflows,has_activities", [(True, False), (False, True), (False, False)])
|
|
4769
|
+
async def test_run_polls_only_registered_task_families(
|
|
4770
|
+
self, mock_client: AsyncMock, has_workflows: bool, has_activities: bool,
|
|
4771
|
+
) -> None:
|
|
4772
|
+
worker = Worker(
|
|
4773
|
+
mock_client,
|
|
4774
|
+
task_queue="q1",
|
|
4775
|
+
workflows=[TestWorkflow] if has_workflows else [],
|
|
4776
|
+
activities=[echo_activity] if has_activities else [],
|
|
4777
|
+
poll_timeout=0.01,
|
|
4778
|
+
)
|
|
4779
|
+
run_task = asyncio.create_task(worker.run())
|
|
4780
|
+
try:
|
|
4781
|
+
await asyncio.sleep(0.05)
|
|
4782
|
+
finally:
|
|
4783
|
+
await worker.stop()
|
|
4784
|
+
await run_task
|
|
4785
|
+
|
|
4786
|
+
assert bool(mock_client.poll_workflow_task.await_count) is has_workflows
|
|
4787
|
+
assert bool(mock_client.poll_activity_task.await_count) is has_activities
|
|
4788
|
+
assert bool(mock_client.poll_query_task.await_count) is has_workflows
|
|
4789
|
+
mock_client.register_worker.assert_awaited_once()
|
|
4790
|
+
mock_client.deregister_worker_registration.assert_awaited_once_with(worker.worker_id)
|
|
4791
|
+
|
|
4732
4792
|
@pytest.mark.asyncio
|
|
4733
4793
|
async def test_run_starts_both_loops(self, mock_client: AsyncMock) -> None:
|
|
4734
4794
|
worker = Worker(
|
|
@@ -4990,6 +5050,33 @@ class TestWorkerHeartbeats:
|
|
|
4990
5050
|
|
|
4991
5051
|
|
|
4992
5052
|
class TestRunUntil:
|
|
5053
|
+
@pytest.mark.asyncio
|
|
5054
|
+
@pytest.mark.parametrize("has_workflows,has_activities", [(True, False), (False, True), (False, False)])
|
|
5055
|
+
async def test_run_until_polls_only_registered_task_families(
|
|
5056
|
+
self, mock_client: AsyncMock, has_workflows: bool, has_activities: bool,
|
|
5057
|
+
) -> None:
|
|
5058
|
+
mock_client.describe_workflow = AsyncMock(side_effect=[
|
|
5059
|
+
WorkflowExecution(workflow_id="wf-1", run_id="run-1", workflow_type="test-wf", status="running"),
|
|
5060
|
+
WorkflowExecution(workflow_id="wf-1", run_id="run-1", workflow_type="test-wf", status="running"),
|
|
5061
|
+
WorkflowExecution(workflow_id="wf-1", run_id="run-1", workflow_type="test-wf", status="completed"),
|
|
5062
|
+
])
|
|
5063
|
+
worker = Worker(
|
|
5064
|
+
mock_client,
|
|
5065
|
+
task_queue="q1",
|
|
5066
|
+
workflows=[TestWorkflow] if has_workflows else [],
|
|
5067
|
+
activities=[echo_activity] if has_activities else [],
|
|
5068
|
+
poll_timeout=0.01,
|
|
5069
|
+
)
|
|
5070
|
+
|
|
5071
|
+
result = await worker.run_until(workflow_id="wf-1", timeout=1.0, poll_interval=0.01)
|
|
5072
|
+
|
|
5073
|
+
assert result.status == "completed"
|
|
5074
|
+
assert bool(mock_client.poll_workflow_task.await_count) is has_workflows
|
|
5075
|
+
assert bool(mock_client.poll_activity_task.await_count) is has_activities
|
|
5076
|
+
if not has_workflows:
|
|
5077
|
+
mock_client.poll_query_task.assert_not_awaited()
|
|
5078
|
+
mock_client.deregister_worker_registration.assert_awaited_once_with(worker.worker_id)
|
|
5079
|
+
|
|
4993
5080
|
@pytest.mark.asyncio
|
|
4994
5081
|
async def test_run_until_returns_terminal_description(self, mock_client: AsyncMock) -> None:
|
|
4995
5082
|
mock_client.describe_workflow = AsyncMock(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_cooperative_cancellation.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_prepared_local_activity.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_sticky_workflow_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/history_bundle_verify.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/replay_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_activity_cancellation_policies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_scope_authoring.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_history.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_policies.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_worker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_runtime_external_payload_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_scoped_run_cancellation_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_update_signal_condition_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_workflow_entrypoint_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|