durable-workflow 2.5.8__tar.gz → 2.5.9__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.8/src/durable_workflow.egg-info → durable_workflow-2.5.9}/PKG-INFO +1 -1
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/pyproject.toml +3 -3
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/client.py +78 -2
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/worker.py +29 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_runtime_external_payload_transport.py +204 -2
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_worker.py +19 -8
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/LICENSE +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/README.md +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/setup.cfg +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/workflow.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_client.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_errors.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_invocable.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_metrics.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_queries.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_replay.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_replay_regression_corpus.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_schedules.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_serializer.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_signals.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_sleep.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_sync.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_updates.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_workflow_updates_conformance.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "2.5.
|
|
7
|
+
version = "2.5.9"
|
|
8
8
|
description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -71,8 +71,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
71
71
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
72
72
|
|
|
73
73
|
[tool.durable-workflow]
|
|
74
|
-
product-train = "2.5.
|
|
75
|
-
registry-version = "2.5.
|
|
74
|
+
product-train = "2.5.9"
|
|
75
|
+
registry-version = "2.5.9"
|
|
76
76
|
supported-server-versions = "2.5.16"
|
|
77
77
|
worker-protocol-version = "1.20"
|
|
78
78
|
control-plane-version = "2"
|
|
@@ -28,7 +28,7 @@ import re
|
|
|
28
28
|
import time
|
|
29
29
|
import uuid
|
|
30
30
|
import warnings
|
|
31
|
-
from collections.abc import AsyncIterator, Callable, Mapping, Sequence
|
|
31
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping, Sequence
|
|
32
32
|
from dataclasses import dataclass
|
|
33
33
|
from importlib.metadata import PackageNotFoundError
|
|
34
34
|
from importlib.metadata import version as _pkg_version
|
|
@@ -1624,6 +1624,7 @@ class Client:
|
|
|
1624
1624
|
_RuntimeExternalPayloadTransport | None
|
|
1625
1625
|
) = None
|
|
1626
1626
|
self._runtime_external_payload_transport_resolved = False
|
|
1627
|
+
self._workflow_payload_observers: dict[str, Callable[[dict[str, Any]], Awaitable[None]]] = {}
|
|
1627
1628
|
|
|
1628
1629
|
async def aclose(self) -> None:
|
|
1629
1630
|
"""Close the underlying ``httpx`` connection pool.
|
|
@@ -1631,6 +1632,7 @@ class Client:
|
|
|
1631
1632
|
Equivalent to exiting the async-context-manager form of the client.
|
|
1632
1633
|
Safe to call multiple times.
|
|
1633
1634
|
"""
|
|
1635
|
+
self._workflow_payload_observers.clear()
|
|
1634
1636
|
await self._http.aclose()
|
|
1635
1637
|
|
|
1636
1638
|
async def __aenter__(self) -> Client:
|
|
@@ -1877,10 +1879,12 @@ class Client:
|
|
|
1877
1879
|
"runtime returned an external payload reference without advertising "
|
|
1878
1880
|
"the authenticated namespace transport"
|
|
1879
1881
|
)
|
|
1880
|
-
result = await self.
|
|
1882
|
+
result = await self._resolve_workflow_claim_payloads(
|
|
1881
1883
|
result,
|
|
1882
1884
|
worker=worker,
|
|
1883
1885
|
transport=transport,
|
|
1886
|
+
path=path,
|
|
1887
|
+
request_json=request_json,
|
|
1884
1888
|
)
|
|
1885
1889
|
result = self._hydrate_result_envelopes(result)
|
|
1886
1890
|
outcome = "ok"
|
|
@@ -2251,6 +2255,77 @@ class Client:
|
|
|
2251
2255
|
self.external_storage_cache.put(reference, data)
|
|
2252
2256
|
return reference
|
|
2253
2257
|
|
|
2258
|
+
async def _resolve_workflow_claim_payloads(
|
|
2259
|
+
self,
|
|
2260
|
+
value: Any,
|
|
2261
|
+
*,
|
|
2262
|
+
worker: bool,
|
|
2263
|
+
transport: _RuntimeExternalPayloadTransport,
|
|
2264
|
+
path: str,
|
|
2265
|
+
request_json: Any,
|
|
2266
|
+
) -> Any:
|
|
2267
|
+
lease: dict[str, Any] | None = None
|
|
2268
|
+
owner: Any = None
|
|
2269
|
+
is_poll = worker and path == "/worker/workflow-tasks/poll"
|
|
2270
|
+
history = re.fullmatch(r"/worker/workflow-tasks/([^/]+)/history", path) if worker else None
|
|
2271
|
+
if isinstance(request_json, dict) and isinstance(value, dict):
|
|
2272
|
+
if is_poll and isinstance(value.get("task"), dict):
|
|
2273
|
+
task = value["task"]
|
|
2274
|
+
if task.get("task_kind", "workflow") == "workflow":
|
|
2275
|
+
lease = dict(task)
|
|
2276
|
+
owner = request_json.get("worker_id")
|
|
2277
|
+
elif history is not None:
|
|
2278
|
+
lease = dict(value)
|
|
2279
|
+
lease.update({
|
|
2280
|
+
"task_id": unquote(history.group(1)),
|
|
2281
|
+
"lease_owner": request_json.get("lease_owner"),
|
|
2282
|
+
"workflow_task_attempt": request_json.get("workflow_task_attempt"),
|
|
2283
|
+
})
|
|
2284
|
+
owner = request_json.get("lease_owner")
|
|
2285
|
+
observer = self._workflow_payload_observers.get(owner) if isinstance(owner, str) else None
|
|
2286
|
+
if lease is None or observer is None:
|
|
2287
|
+
return await self._resolve_runtime_payloads(value, worker=worker, transport=transport)
|
|
2288
|
+
if (not isinstance(lease.get("task_id"), str) or not lease["task_id"]
|
|
2289
|
+
or lease.get("lease_owner", owner) != owner
|
|
2290
|
+
or type(lease.get("workflow_task_attempt", 1)) is not int
|
|
2291
|
+
or lease.get("workflow_task_attempt", 1) < 1
|
|
2292
|
+
or lease.get("namespace", self.namespace) != self.namespace):
|
|
2293
|
+
raise ExternalPayloadUnsupported("workflow payload response has an invalid task ownership fence")
|
|
2294
|
+
lease.setdefault("lease_owner", owner)
|
|
2295
|
+
lease.setdefault("workflow_task_attempt", 1)
|
|
2296
|
+
|
|
2297
|
+
async def confirm() -> None:
|
|
2298
|
+
await asyncio.wait_for(observer(lease), timeout=5.0)
|
|
2299
|
+
|
|
2300
|
+
async def observe() -> None:
|
|
2301
|
+
while True:
|
|
2302
|
+
await asyncio.sleep(5.0)
|
|
2303
|
+
await confirm()
|
|
2304
|
+
|
|
2305
|
+
# The poll body is now available, but application code cannot run until
|
|
2306
|
+
# its referenced bytes arrive and the same claim is confirmed again.
|
|
2307
|
+
await confirm()
|
|
2308
|
+
resolution = asyncio.create_task(self._resolve_runtime_payloads(value, worker=worker, transport=transport))
|
|
2309
|
+
observation = asyncio.create_task(observe())
|
|
2310
|
+
try:
|
|
2311
|
+
done, _ = await asyncio.wait({resolution, observation}, return_when=asyncio.FIRST_COMPLETED)
|
|
2312
|
+
if observation in done:
|
|
2313
|
+
await observation
|
|
2314
|
+
raise ExternalPayloadUnavailable("workflow payload lease observer stopped")
|
|
2315
|
+
result = await resolution
|
|
2316
|
+
observation.cancel()
|
|
2317
|
+
await asyncio.gather(observation, return_exceptions=True)
|
|
2318
|
+
await confirm()
|
|
2319
|
+
if isinstance(result, dict) and "cancellation_request" in lease:
|
|
2320
|
+
target = result.get("task") if is_poll else result
|
|
2321
|
+
if isinstance(target, dict):
|
|
2322
|
+
target["cancellation_request"] = lease["cancellation_request"]
|
|
2323
|
+
return result
|
|
2324
|
+
finally:
|
|
2325
|
+
resolution.cancel()
|
|
2326
|
+
observation.cancel()
|
|
2327
|
+
await asyncio.gather(resolution, observation, return_exceptions=True)
|
|
2328
|
+
|
|
2254
2329
|
async def _resolve_runtime_payloads(
|
|
2255
2330
|
self,
|
|
2256
2331
|
value: Any,
|
|
@@ -2689,6 +2764,7 @@ class Client:
|
|
|
2689
2764
|
scoped._cluster_info_lock = asyncio.Lock()
|
|
2690
2765
|
scoped._runtime_external_payload_transport_cache = None
|
|
2691
2766
|
scoped._runtime_external_payload_transport_resolved = False
|
|
2767
|
+
scoped._workflow_payload_observers = {}
|
|
2692
2768
|
scoped.external_storage = None
|
|
2693
2769
|
scoped.external_storage_threshold_bytes = None
|
|
2694
2770
|
scoped.external_storage_cache = ExternalPayloadCache()
|
|
@@ -1519,6 +1519,28 @@ class Worker:
|
|
|
1519
1519
|
|
|
1520
1520
|
raise RuntimeError("workflow yielded too many consecutive Nexus service calls")
|
|
1521
1521
|
|
|
1522
|
+
async def _renew_workflow_payload_lease(self, task: dict[str, Any]) -> None:
|
|
1523
|
+
if self._stop.is_set() or self._local_activity_shutdown.is_set():
|
|
1524
|
+
raise LocalActivityExecutionAborted("worker shutdown stopped its workflow payload transfer")
|
|
1525
|
+
response = await self.client.heartbeat_workflow_task(
|
|
1526
|
+
task_id=task["task_id"], lease_owner=self.worker_id,
|
|
1527
|
+
workflow_task_attempt=task["workflow_task_attempt"],
|
|
1528
|
+
)
|
|
1529
|
+
if self._stop.is_set() or self._local_activity_shutdown.is_set():
|
|
1530
|
+
raise LocalActivityExecutionAborted("worker shutdown stopped its workflow payload transfer")
|
|
1531
|
+
if not isinstance(response, Mapping) or any((
|
|
1532
|
+
response.get("task_id") != task["task_id"],
|
|
1533
|
+
response.get("lease_owner") != self.worker_id,
|
|
1534
|
+
response.get("workflow_task_attempt") != task["workflow_task_attempt"],
|
|
1535
|
+
response.get("renewed") is not True,
|
|
1536
|
+
)):
|
|
1537
|
+
raise LocalActivityExecutionAborted("workflow payload lease was not fenced and acknowledged")
|
|
1538
|
+
observation = response.get("cancellation_request")
|
|
1539
|
+
if observation is not None:
|
|
1540
|
+
# Preserve the observation for the normal canonical delivery path.
|
|
1541
|
+
# Hydrating a payload does not deliver or acknowledge cancellation.
|
|
1542
|
+
self._observe_workflow_cancellation(task, observation)
|
|
1543
|
+
|
|
1522
1544
|
async def _renew_local_workflow_lease(self, task: dict[str, Any]) -> None:
|
|
1523
1545
|
if self._cooperative_cancellation_supported and self._local_activity_shutdown.is_set():
|
|
1524
1546
|
raise LocalActivityExecutionAborted("worker shutdown abandoned its local workflow claim")
|
|
@@ -4414,6 +4436,9 @@ class Worker:
|
|
|
4414
4436
|
if self._stop.is_set():
|
|
4415
4437
|
raise RuntimeError("worker cannot run after stop() has been called")
|
|
4416
4438
|
self._run_started = True
|
|
4439
|
+
payload_observers = getattr(self.client, "_workflow_payload_observers", None)
|
|
4440
|
+
if isinstance(payload_observers, dict):
|
|
4441
|
+
payload_observers[self.worker_id] = self._renew_workflow_payload_lease
|
|
4417
4442
|
|
|
4418
4443
|
def worker_session(self, options: WorkerSessionOptions) -> WorkerSession:
|
|
4419
4444
|
"""Return a typed lease handle held by this worker until shutdown."""
|
|
@@ -4532,6 +4557,10 @@ class Worker:
|
|
|
4532
4557
|
self._registered = False
|
|
4533
4558
|
log.info("worker %s deregistered", self.worker_id)
|
|
4534
4559
|
self._sticky_cache.clear()
|
|
4560
|
+
payload_observers = getattr(self.client, "_workflow_payload_observers", None)
|
|
4561
|
+
if (isinstance(payload_observers, dict)
|
|
4562
|
+
and payload_observers.get(self.worker_id) == self._renew_workflow_payload_lease):
|
|
4563
|
+
payload_observers.pop(self.worker_id)
|
|
4535
4564
|
|
|
4536
4565
|
@staticmethod
|
|
4537
4566
|
def _remaining_shutdown_time(deadline: float) -> float:
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_runtime_external_payload_transport.py
RENAMED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import gc
|
|
3
5
|
import hashlib
|
|
4
6
|
import json
|
|
7
|
+
import weakref
|
|
5
8
|
from typing import Any
|
|
6
9
|
|
|
7
10
|
import httpx
|
|
8
11
|
import pytest
|
|
9
12
|
|
|
10
|
-
from durable_workflow import Replayer, serializer, workflow
|
|
13
|
+
from durable_workflow import Replayer, Worker, serializer, workflow
|
|
11
14
|
from durable_workflow.client import Client
|
|
12
15
|
from durable_workflow.errors import (
|
|
13
16
|
ExternalPayloadError,
|
|
@@ -26,7 +29,7 @@ from durable_workflow.external_storage import (
|
|
|
26
29
|
ExternalPayloadCache,
|
|
27
30
|
)
|
|
28
31
|
from durable_workflow.retry_policy import TransportRetryPolicy
|
|
29
|
-
from durable_workflow.workflow import CompleteWorkflow, WorkflowContext
|
|
32
|
+
from durable_workflow.workflow import CompleteWorkflow, LocalActivityExecutionAborted, WorkflowContext
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
@workflow.defn(name="runtime-payload-replay")
|
|
@@ -309,6 +312,205 @@ def runtime_envelope(server: FakeRuntimePayloadServer, value: Any) -> dict[str,
|
|
|
309
312
|
return {"codec": "avro", "external_payload": server.seed(blob)}
|
|
310
313
|
|
|
311
314
|
|
|
315
|
+
def payload_worker(client: Client, *, capabilities: list[str] | None = None) -> Worker:
|
|
316
|
+
worker = Worker(client, task_queue="billing", worker_id="worker-1", workflows=[RuntimePayloadReplayWorkflow],
|
|
317
|
+
capabilities=capabilities or [])
|
|
318
|
+
worker._begin_run()
|
|
319
|
+
worker._registration_done.set()
|
|
320
|
+
return worker
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@pytest.mark.asyncio
|
|
324
|
+
@pytest.mark.parametrize("history_page", [False, True])
|
|
325
|
+
async def test_worker_confirms_original_lease_before_and_after_payload_hydration(history_page: bool) -> None:
|
|
326
|
+
server = FakeRuntimePayloadServer()
|
|
327
|
+
client = runtime_client(server)
|
|
328
|
+
payload_worker(client)
|
|
329
|
+
task = {"task_id": "task-1", "lease_owner": "worker-1", "workflow_task_attempt": 3,
|
|
330
|
+
"arguments": runtime_envelope(server, ["original-input"])}
|
|
331
|
+
path = "/api/worker/workflow-tasks/task-1/history" if history_page else "/api/worker/workflow-tasks/poll"
|
|
332
|
+
server.responses[("POST", path)] = {"history_events": [task]} if history_page else {"task": task}
|
|
333
|
+
server.responses[("POST", "/api/worker/workflow-tasks/task-1/heartbeat")] = {
|
|
334
|
+
"task_id": "task-1", "lease_owner": "worker-1", "workflow_task_attempt": 3, "renewed": True,
|
|
335
|
+
}
|
|
336
|
+
original_handler = server.handler
|
|
337
|
+
|
|
338
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
339
|
+
if request.method == "GET" and "/external-payloads/" in request.url.path:
|
|
340
|
+
assert len([entry for entry in server.requests if entry[1].endswith("/heartbeat")]) == 1
|
|
341
|
+
return original_handler(request)
|
|
342
|
+
|
|
343
|
+
client._http = httpx.AsyncClient(base_url=client.base_url, transport=httpx.MockTransport(handler))
|
|
344
|
+
try:
|
|
345
|
+
if history_page:
|
|
346
|
+
result = await client.workflow_task_history(task_id="task-1", lease_owner="worker-1",
|
|
347
|
+
workflow_task_attempt=3, next_history_page_token="page-2")
|
|
348
|
+
resolved = result["history_events"][0]
|
|
349
|
+
else:
|
|
350
|
+
result = await client.poll_workflow_task(worker_id="worker-1", task_queue="billing", timeout=0)
|
|
351
|
+
resolved = result
|
|
352
|
+
assert serializer.decode_envelope(resolved["arguments"]) == ["original-input"]
|
|
353
|
+
heartbeats = [entry for entry in server.requests if entry[1].endswith("/heartbeat")]
|
|
354
|
+
assert len(heartbeats) == 2
|
|
355
|
+
assert all(entry[2] == {"lease_owner": "worker-1", "workflow_task_attempt": 3} for entry in heartbeats)
|
|
356
|
+
finally:
|
|
357
|
+
await client.aclose()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@pytest.mark.asyncio
|
|
361
|
+
@pytest.mark.parametrize("changed_fence", [
|
|
362
|
+
{"workflow_task_attempt": 4}, {"lease_owner": "worker-2"}, {"renewed": False},
|
|
363
|
+
])
|
|
364
|
+
async def test_worker_does_not_fetch_payload_when_original_claim_is_not_confirmed(
|
|
365
|
+
changed_fence: dict[str, Any],
|
|
366
|
+
) -> None:
|
|
367
|
+
server = FakeRuntimePayloadServer()
|
|
368
|
+
client = runtime_client(server)
|
|
369
|
+
payload_worker(client)
|
|
370
|
+
server.responses[("POST", "/api/worker/workflow-tasks/poll")] = {"task": {
|
|
371
|
+
"task_id": "task-1", "lease_owner": "worker-1", "workflow_task_attempt": 3,
|
|
372
|
+
"arguments": runtime_envelope(server, ["never-executed"]),
|
|
373
|
+
}}
|
|
374
|
+
server.responses[("POST", "/api/worker/workflow-tasks/task-1/heartbeat")] = {
|
|
375
|
+
"task_id": "task-1", "lease_owner": "worker-1", "workflow_task_attempt": 3, "renewed": True, **changed_fence,
|
|
376
|
+
}
|
|
377
|
+
try:
|
|
378
|
+
with pytest.raises(LocalActivityExecutionAborted, match="workflow payload lease was not fenced"):
|
|
379
|
+
await client.poll_workflow_task(worker_id="worker-1", task_queue="billing", timeout=0)
|
|
380
|
+
assert server.fetch_count == 0
|
|
381
|
+
finally:
|
|
382
|
+
await client.aclose()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@pytest.mark.asyncio
|
|
386
|
+
async def test_payload_renewal_preserves_new_cancellation_for_canonical_delivery() -> None:
|
|
387
|
+
from tests.test_cooperative_cancellation import observation
|
|
388
|
+
|
|
389
|
+
server = FakeRuntimePayloadServer()
|
|
390
|
+
client = runtime_client(server)
|
|
391
|
+
worker = payload_worker(client, capabilities=["cooperative_cancellation"])
|
|
392
|
+
worker._cooperative_cancellation_supported = True
|
|
393
|
+
original = observation()
|
|
394
|
+
server.responses[("POST", "/api/worker/workflow-tasks/poll")] = {"task": {
|
|
395
|
+
"task_id": "task-1", "lease_owner": "worker-1", "workflow_task_attempt": 3,
|
|
396
|
+
"arguments": runtime_envelope(server, ["original-input"]),
|
|
397
|
+
}}
|
|
398
|
+
server.responses[("POST", "/api/worker/workflow-tasks/task-1/heartbeat")] = {
|
|
399
|
+
"task_id": "task-1", "lease_owner": "worker-1", "workflow_task_attempt": 3, "renewed": True,
|
|
400
|
+
"cancellation_request": original,
|
|
401
|
+
}
|
|
402
|
+
try:
|
|
403
|
+
result = await client.poll_workflow_task(worker_id="worker-1", task_queue="billing", timeout=0)
|
|
404
|
+
assert result["cancellation_request"] == original
|
|
405
|
+
assert "_delivered_cancellation_request_id" not in result
|
|
406
|
+
assert not any("/cancellation/" in entry[1] for entry in server.requests)
|
|
407
|
+
finally:
|
|
408
|
+
await client.aclose()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@pytest.mark.asyncio
|
|
412
|
+
async def test_stopped_worker_does_not_start_claim_payload_download() -> None:
|
|
413
|
+
server = FakeRuntimePayloadServer()
|
|
414
|
+
client = runtime_client(server)
|
|
415
|
+
worker = payload_worker(client)
|
|
416
|
+
worker._stop.set()
|
|
417
|
+
server.responses[("POST", "/api/worker/workflow-tasks/poll")] = {"task": {
|
|
418
|
+
"task_id": "task-1", "lease_owner": "worker-1", "workflow_task_attempt": 3,
|
|
419
|
+
"arguments": runtime_envelope(server, ["never-executed"]),
|
|
420
|
+
}}
|
|
421
|
+
try:
|
|
422
|
+
with pytest.raises(LocalActivityExecutionAborted, match="shutdown stopped"):
|
|
423
|
+
await client.poll_workflow_task(worker_id="worker-1", task_queue="billing", timeout=0)
|
|
424
|
+
assert server.fetch_count == 0
|
|
425
|
+
assert not any(entry[1].endswith("/heartbeat") for entry in server.requests)
|
|
426
|
+
finally:
|
|
427
|
+
await client.aclose()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@pytest.mark.asyncio
|
|
431
|
+
async def test_payload_transfer_is_cancelled_when_its_periodic_lease_observer_loses_authority(
|
|
432
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
433
|
+
) -> None:
|
|
434
|
+
server = FakeRuntimePayloadServer()
|
|
435
|
+
client = runtime_client(server)
|
|
436
|
+
payload_worker(client)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
server.responses[("POST", "/api/worker/workflow-tasks/poll")] = {"task": {
|
|
440
|
+
"task_id": "task-1", "lease_owner": "worker-1", "workflow_task_attempt": 3,
|
|
441
|
+
"arguments": runtime_envelope(server, ["never-executed"]),
|
|
442
|
+
}}
|
|
443
|
+
fetch_started = asyncio.Event()
|
|
444
|
+
fetch_cancelled = asyncio.Event()
|
|
445
|
+
observations = 0
|
|
446
|
+
|
|
447
|
+
async def heartbeat(request: httpx.Request) -> httpx.Response:
|
|
448
|
+
nonlocal observations
|
|
449
|
+
if request.url.path.endswith("/heartbeat"):
|
|
450
|
+
observations += 1
|
|
451
|
+
if observations > 1:
|
|
452
|
+
return httpx.Response(409, json={"reason": "lease_expired"})
|
|
453
|
+
return httpx.Response(200, json={"task_id": "task-1", "lease_owner": "worker-1",
|
|
454
|
+
"workflow_task_attempt": 3, "renewed": True})
|
|
455
|
+
if request.method == "GET" and "/external-payloads/" in request.url.path:
|
|
456
|
+
fetch_started.set()
|
|
457
|
+
try:
|
|
458
|
+
await asyncio.Event().wait()
|
|
459
|
+
finally:
|
|
460
|
+
fetch_cancelled.set()
|
|
461
|
+
return server.handler(request)
|
|
462
|
+
|
|
463
|
+
real_sleep = asyncio.sleep
|
|
464
|
+
|
|
465
|
+
async def prompt_observation(delay: float) -> None:
|
|
466
|
+
if delay == 5.0:
|
|
467
|
+
await fetch_started.wait()
|
|
468
|
+
await real_sleep(0)
|
|
469
|
+
else:
|
|
470
|
+
await real_sleep(delay)
|
|
471
|
+
|
|
472
|
+
monkeypatch.setattr("durable_workflow.client.asyncio.sleep", prompt_observation)
|
|
473
|
+
client._http = httpx.AsyncClient(base_url=client.base_url, transport=httpx.MockTransport(heartbeat))
|
|
474
|
+
try:
|
|
475
|
+
with pytest.raises(ServerError) as failure:
|
|
476
|
+
await asyncio.wait_for(client.poll_workflow_task(worker_id="worker-1", task_queue="billing", timeout=0), 2)
|
|
477
|
+
assert failure.value.reason() == "lease_expired"
|
|
478
|
+
assert fetch_cancelled.is_set()
|
|
479
|
+
assert observations == 2
|
|
480
|
+
finally:
|
|
481
|
+
await client.aclose()
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@pytest.mark.asyncio
|
|
485
|
+
async def test_shared_client_does_not_retain_unstarted_or_stopped_workers() -> None:
|
|
486
|
+
client = runtime_client(FakeRuntimePayloadServer())
|
|
487
|
+
worker = Worker(client, task_queue="billing", worker_id="worker-1", workflows=[RuntimePayloadReplayWorkflow])
|
|
488
|
+
assert client._workflow_payload_observers == {}
|
|
489
|
+
reference = weakref.ref(worker)
|
|
490
|
+
worker._begin_run()
|
|
491
|
+
worker._registration_done.set()
|
|
492
|
+
assert "worker-1" in client._workflow_payload_observers
|
|
493
|
+
await worker.stop()
|
|
494
|
+
assert client._workflow_payload_observers == {}
|
|
495
|
+
del worker
|
|
496
|
+
await asyncio.sleep(0)
|
|
497
|
+
gc.collect()
|
|
498
|
+
assert reference() is None
|
|
499
|
+
await client.aclose()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@pytest.mark.asyncio
|
|
503
|
+
async def test_stopping_old_worker_preserves_a_new_worker_observer() -> None:
|
|
504
|
+
client = runtime_client(FakeRuntimePayloadServer())
|
|
505
|
+
older = payload_worker(client)
|
|
506
|
+
newer = payload_worker(client)
|
|
507
|
+
await older.stop()
|
|
508
|
+
assert client._workflow_payload_observers["worker-1"] == newer._renew_workflow_payload_lease
|
|
509
|
+
await newer.stop()
|
|
510
|
+
assert client._workflow_payload_observers == {}
|
|
511
|
+
await client.aclose()
|
|
512
|
+
|
|
513
|
+
|
|
312
514
|
@pytest.mark.asyncio
|
|
313
515
|
@pytest.mark.parametrize("threshold_bytes", [1, 4096])
|
|
314
516
|
async def test_nexus_arguments_offload_without_changing_recorded_arguments(threshold_bytes: int) -> None:
|
|
@@ -4955,9 +4955,18 @@ class TestWorkerHeartbeats:
|
|
|
4955
4955
|
async def test_heartbeat_loop_survives_transient_errors(
|
|
4956
4956
|
self, mock_client: AsyncMock
|
|
4957
4957
|
) -> None:
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4958
|
+
recovered = asyncio.Event()
|
|
4959
|
+
attempts = 0
|
|
4960
|
+
|
|
4961
|
+
async def heartbeat(**kwargs: object) -> dict[str, bool]:
|
|
4962
|
+
nonlocal attempts
|
|
4963
|
+
attempts += 1
|
|
4964
|
+
if attempts == 1:
|
|
4965
|
+
raise RuntimeError("temporary")
|
|
4966
|
+
recovered.set()
|
|
4967
|
+
return {"acknowledged": True}
|
|
4968
|
+
|
|
4969
|
+
mock_client.heartbeat_worker = AsyncMock(side_effect=heartbeat)
|
|
4961
4970
|
worker = Worker(
|
|
4962
4971
|
mock_client,
|
|
4963
4972
|
task_queue="q1",
|
|
@@ -4967,11 +4976,13 @@ class TestWorkerHeartbeats:
|
|
|
4967
4976
|
heartbeat_interval=0.02,
|
|
4968
4977
|
)
|
|
4969
4978
|
run_task = asyncio.create_task(worker.run())
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4979
|
+
try:
|
|
4980
|
+
await asyncio.wait_for(recovered.wait(), timeout=2)
|
|
4981
|
+
finally:
|
|
4982
|
+
await worker.stop()
|
|
4983
|
+
run_task.cancel()
|
|
4984
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
4985
|
+
await run_task
|
|
4975
4986
|
assert mock_client.heartbeat_worker.call_count >= 2
|
|
4976
4987
|
|
|
4977
4988
|
def test_process_metrics_cpu_percent_is_instantaneous_not_lifetime(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/_cooperative_cancellation.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/_prepared_local_activity.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/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.8 → durable_workflow-2.5.9}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/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.8 → durable_workflow-2.5.9}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/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.8 → durable_workflow-2.5.9}/src/durable_workflow.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/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.8 → durable_workflow-2.5.9}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cancellation_scope_authoring.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_committed_cancellation_scope_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_committed_cancellation_scope_history.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_committed_cancellation_scope_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cooperative_cancellation_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_cooperative_cancellation_worker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_prepared_local_activity_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_prepared_local_activity_policies.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/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
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/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.8 → durable_workflow-2.5.9}/tests/test_update_signal_condition_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_workflow_entrypoint_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.8 → durable_workflow-2.5.9}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|