durable-workflow 2.5.4__tar.gz → 2.6.0rc2__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.4/src/durable_workflow.egg-info → durable_workflow-2.6.0rc2}/PKG-INFO +1 -1
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/pyproject.toml +3 -3
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/client.py +53 -10
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/nexus.py +15 -2
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/worker.py +1 -121
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/workflow.py +161 -11
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow.egg-info/SOURCES.txt +3 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_client.py +3 -2
- durable_workflow-2.6.0rc2/tests/test_nexus_durable_wait.py +156 -0
- durable_workflow-2.6.0rc2/tests/test_nexus_namespace.py +160 -0
- durable_workflow-2.6.0rc2/tests/test_nexus_wire.py +38 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_replay_regression_corpus.py +22 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_runtime_external_payload_transport.py +49 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_worker.py +21 -58
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/LICENSE +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/README.md +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/setup.cfg +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_errors.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_invocable.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_metrics.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_queries.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_replay.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_schedules.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_serializer.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_signals.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_sleep.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_sync.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_updates.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.5.4 → durable_workflow-2.6.0rc2}/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.
|
|
7
|
+
version = "2.6.0-rc.2"
|
|
8
8
|
description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -71,8 +71,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
71
71
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
72
72
|
|
|
73
73
|
[tool.durable-workflow]
|
|
74
|
-
product-train = "2.
|
|
75
|
-
registry-version = "2.
|
|
74
|
+
product-train = "2.6.0-rc.2"
|
|
75
|
+
registry-version = "2.6.0rc2"
|
|
76
76
|
supported-server-versions = "2.5.16"
|
|
77
77
|
worker-protocol-version = "1.19"
|
|
78
78
|
control-plane-version = "2"
|
|
@@ -19,6 +19,7 @@ methods without repeating the id on every call.
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import asyncio
|
|
22
|
+
import copy
|
|
22
23
|
import hashlib
|
|
23
24
|
import json as json_module
|
|
24
25
|
import math
|
|
@@ -299,8 +300,8 @@ def _contains_inline_payload_envelope(value: object) -> bool:
|
|
|
299
300
|
if set(value) == {"codec", "blob"} and isinstance(value.get("blob"), str):
|
|
300
301
|
return True
|
|
301
302
|
if (
|
|
302
|
-
value.get("type") == "record_side_effect"
|
|
303
|
-
and isinstance(value.get("
|
|
303
|
+
(value.get("type") == "record_side_effect" and isinstance(value.get("result"), str))
|
|
304
|
+
or (value.get("type") == "start_service_operation" and isinstance(value.get("request_payload"), str))
|
|
304
305
|
):
|
|
305
306
|
return True
|
|
306
307
|
return any(_contains_inline_payload_envelope(item) for item in value.values())
|
|
@@ -2079,21 +2080,23 @@ class Client:
|
|
|
2079
2080
|
slot: tuple[str | int, ...] = (),
|
|
2080
2081
|
) -> Any:
|
|
2081
2082
|
if isinstance(value, dict):
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2083
|
+
command_type = value.get("type")
|
|
2084
|
+
blob_key = (
|
|
2085
|
+
{"record_side_effect": "result", "start_service_operation": "request_payload"}.get(command_type)
|
|
2086
|
+
if isinstance(command_type, str) else None
|
|
2087
|
+
)
|
|
2088
|
+
if blob_key is not None and isinstance(value.get(blob_key), str):
|
|
2086
2089
|
normalized_command = dict(value)
|
|
2087
2090
|
externalized_result = await self._externalize_runtime_payloads(
|
|
2088
|
-
{"codec": serializer.AVRO_CODEC, "blob": value[
|
|
2091
|
+
{"codec": value.get("payload_codec", serializer.AVRO_CODEC), "blob": value[blob_key]},
|
|
2089
2092
|
worker=worker,
|
|
2090
2093
|
transport=transport,
|
|
2091
2094
|
uploaded=uploaded,
|
|
2092
2095
|
completion=completion,
|
|
2093
|
-
slot=(*slot,
|
|
2096
|
+
slot=(*slot, blob_key),
|
|
2094
2097
|
)
|
|
2095
2098
|
if "external_payload" in externalized_result:
|
|
2096
|
-
normalized_command[
|
|
2099
|
+
normalized_command[blob_key] = externalized_result
|
|
2097
2100
|
value = normalized_command
|
|
2098
2101
|
|
|
2099
2102
|
if set(value) == {"codec", "blob"} and isinstance(value.get("blob"), str):
|
|
@@ -2672,6 +2675,26 @@ class Client:
|
|
|
2672
2675
|
"""
|
|
2673
2676
|
return WorkflowHandle(self, workflow_id=workflow_id, run_id=run_id, workflow_type=workflow_type)
|
|
2674
2677
|
|
|
2678
|
+
def _nexus_target_client(self, target_namespace: str | None) -> Client:
|
|
2679
|
+
if target_namespace is not None and not target_namespace.strip():
|
|
2680
|
+
raise ValueError("target_namespace must be nonempty")
|
|
2681
|
+
if target_namespace is None or target_namespace == self.namespace:
|
|
2682
|
+
return self
|
|
2683
|
+
self._auth_token(worker=False)
|
|
2684
|
+
scoped = copy.copy(self)
|
|
2685
|
+
scoped.namespace = target_namespace
|
|
2686
|
+
# Share the HTTP pool, never the caller's namespace discovery or
|
|
2687
|
+
# provider credentials. The worker keeps using the original client.
|
|
2688
|
+
scoped.worker_token = None
|
|
2689
|
+
scoped._cluster_info = None
|
|
2690
|
+
scoped._cluster_info_lock = asyncio.Lock()
|
|
2691
|
+
scoped._runtime_external_payload_transport_cache = None
|
|
2692
|
+
scoped._runtime_external_payload_transport_resolved = False
|
|
2693
|
+
scoped.external_storage = None
|
|
2694
|
+
scoped.external_storage_threshold_bytes = None
|
|
2695
|
+
scoped.external_storage_cache = ExternalPayloadCache()
|
|
2696
|
+
return scoped
|
|
2697
|
+
|
|
2675
2698
|
async def execute_nexus_operation(
|
|
2676
2699
|
self,
|
|
2677
2700
|
endpoint_name: str,
|
|
@@ -2685,6 +2708,7 @@ class Client:
|
|
|
2685
2708
|
idempotency_key: str | None = None,
|
|
2686
2709
|
payload_codec: str | None = None,
|
|
2687
2710
|
caller_namespace: str | None = None,
|
|
2711
|
+
target_namespace: str | None = None,
|
|
2688
2712
|
caller_workflow_instance_id: str | None = None,
|
|
2689
2713
|
caller_workflow_run_id: str | None = None,
|
|
2690
2714
|
service_sdk_language: str | None = None,
|
|
@@ -2708,7 +2732,11 @@ class Client:
|
|
|
2708
2732
|
the worker uses this client method to make the single durable
|
|
2709
2733
|
execute request and records the returned service-call surface into
|
|
2710
2734
|
workflow history.
|
|
2735
|
+
|
|
2736
|
+
``target_namespace`` selects the service catalog and its payload
|
|
2737
|
+
storage. The caller identity and worker namespace are preserved.
|
|
2711
2738
|
"""
|
|
2739
|
+
execution_client = self._nexus_target_client(target_namespace)
|
|
2712
2740
|
request_body = nexus_request_payload(
|
|
2713
2741
|
arguments=list(arguments) if arguments is not None else [],
|
|
2714
2742
|
payload_codec=payload_codec,
|
|
@@ -2736,8 +2764,21 @@ class Client:
|
|
|
2736
2764
|
)
|
|
2737
2765
|
context = f"{endpoint_name}/{service_name}/{operation_name}"
|
|
2738
2766
|
|
|
2767
|
+
wire_body = request_body
|
|
2768
|
+
transport = await execution_client._runtime_external_payload_transport()
|
|
2769
|
+
if transport is not None:
|
|
2770
|
+
arguments_envelope = await execution_client._externalize_runtime_payloads(
|
|
2771
|
+
{"codec": request_body["payload_codec"], "blob": request_body["arguments"]},
|
|
2772
|
+
worker=False,
|
|
2773
|
+
transport=transport,
|
|
2774
|
+
uploaded={},
|
|
2775
|
+
slot=("arguments",),
|
|
2776
|
+
)
|
|
2777
|
+
if "external_payload" in arguments_envelope:
|
|
2778
|
+
wire_body = {**request_body, "arguments": arguments_envelope}
|
|
2779
|
+
|
|
2739
2780
|
try:
|
|
2740
|
-
data = await
|
|
2781
|
+
data = await execution_client._request("POST", path, json=wire_body, context=context)
|
|
2741
2782
|
except ServerError as exc:
|
|
2742
2783
|
if exc.status != 409 or not isinstance(exc.body, dict):
|
|
2743
2784
|
raise
|
|
@@ -2747,6 +2788,7 @@ class Client:
|
|
|
2747
2788
|
service_name=service_name,
|
|
2748
2789
|
operation_name=operation_name,
|
|
2749
2790
|
request_payload=request_body,
|
|
2791
|
+
target_namespace=target_namespace,
|
|
2750
2792
|
service_sdk_language=service_sdk_language,
|
|
2751
2793
|
artifact_tuple=artifact_tuple,
|
|
2752
2794
|
published_artifact_worker_execution=published_artifact_worker_execution,
|
|
@@ -2770,6 +2812,7 @@ class Client:
|
|
|
2770
2812
|
service_name=service_name,
|
|
2771
2813
|
operation_name=operation_name,
|
|
2772
2814
|
request_payload=request_body,
|
|
2815
|
+
target_namespace=target_namespace,
|
|
2773
2816
|
service_sdk_language=service_sdk_language,
|
|
2774
2817
|
artifact_tuple=artifact_tuple,
|
|
2775
2818
|
published_artifact_worker_execution=published_artifact_worker_execution,
|
|
@@ -8,6 +8,7 @@ from collections.abc import Mapping, Sequence
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
from . import serializer
|
|
11
12
|
from .errors import NexusOperationFailed
|
|
12
13
|
|
|
13
14
|
NEXUS_OPERATION_RESULT_SCHEMA = "durable-workflow.v2.sdk-python.nexus-operation-result"
|
|
@@ -49,6 +50,7 @@ def stable_nexus_idempotency_key(
|
|
|
49
50
|
service_name: str,
|
|
50
51
|
operation_name: str,
|
|
51
52
|
arguments: Sequence[Any],
|
|
53
|
+
target_namespace: str | None = None,
|
|
52
54
|
) -> str:
|
|
53
55
|
"""Return a deterministic idempotency key for one workflow-side Nexus call."""
|
|
54
56
|
source = {
|
|
@@ -61,6 +63,8 @@ def stable_nexus_idempotency_key(
|
|
|
61
63
|
"operation_name": operation_name,
|
|
62
64
|
"arguments": list(arguments),
|
|
63
65
|
}
|
|
66
|
+
if target_namespace is not None:
|
|
67
|
+
source["target_namespace"] = target_namespace
|
|
64
68
|
encoded = json.dumps(source, sort_keys=True, separators=(",", ":"), default=_json_default)
|
|
65
69
|
return f"dw-py-nexus-{hashlib.sha256(encoded.encode()).hexdigest()}"
|
|
66
70
|
|
|
@@ -87,9 +91,10 @@ def nexus_request_payload(
|
|
|
87
91
|
duplicate_start_policy: str | None = None,
|
|
88
92
|
) -> dict[str, Any]:
|
|
89
93
|
"""Build the server execute-operation request body."""
|
|
94
|
+
codec = payload_codec or serializer.AVRO_CODEC
|
|
90
95
|
return _clean_dict({
|
|
91
|
-
"arguments": list(arguments),
|
|
92
|
-
"payload_codec":
|
|
96
|
+
"arguments": serializer.encode(list(arguments), codec=codec),
|
|
97
|
+
"payload_codec": codec,
|
|
93
98
|
"mode_override": mode,
|
|
94
99
|
"wait_for": wait_for,
|
|
95
100
|
"wait_timeout_seconds": wait_timeout_seconds,
|
|
@@ -134,6 +139,7 @@ class NexusOperationResult:
|
|
|
134
139
|
caller_observed_error_type: str | None = None
|
|
135
140
|
typed_error_message: str | None = None
|
|
136
141
|
result: Any = None
|
|
142
|
+
target_namespace: str | None = None
|
|
137
143
|
|
|
138
144
|
@classmethod
|
|
139
145
|
def from_response(
|
|
@@ -147,6 +153,7 @@ class NexusOperationResult:
|
|
|
147
153
|
service_sdk_language: str | None = None,
|
|
148
154
|
artifact_tuple: Mapping[str, Any] | None = None,
|
|
149
155
|
published_artifact_worker_execution: bool | None = None,
|
|
156
|
+
target_namespace: str | None = None,
|
|
150
157
|
) -> NexusOperationResult:
|
|
151
158
|
call = data.get("service_call")
|
|
152
159
|
source = call if isinstance(call, Mapping) else data
|
|
@@ -188,6 +195,7 @@ class NexusOperationResult:
|
|
|
188
195
|
typed_error_message=_optional_str(source.get("typed_error_message"))
|
|
189
196
|
or _optional_str(data.get("typed_error_message")),
|
|
190
197
|
result=response_value,
|
|
198
|
+
target_namespace=target_namespace,
|
|
191
199
|
)
|
|
192
200
|
|
|
193
201
|
@classmethod
|
|
@@ -199,6 +207,9 @@ class NexusOperationResult:
|
|
|
199
207
|
if value.get("version") != NEXUS_OPERATION_RESULT_VERSION:
|
|
200
208
|
raise ValueError("recorded Nexus operation result version is not supported")
|
|
201
209
|
|
|
210
|
+
if value.get("target_namespace") != expected.get("target_namespace"):
|
|
211
|
+
raise ValueError("recorded Nexus target namespace does not match the current workflow command")
|
|
212
|
+
|
|
202
213
|
for key in ("endpoint_name", "service_name", "operation_name", "idempotency_key"):
|
|
203
214
|
expected_value = expected.get(key)
|
|
204
215
|
recorded_value = value.get(key)
|
|
@@ -235,6 +246,7 @@ class NexusOperationResult:
|
|
|
235
246
|
caller_observed_error_type=_optional_str(value.get("caller_observed_error_type")),
|
|
236
247
|
typed_error_message=_optional_str(value.get("typed_error_message")),
|
|
237
248
|
result=value.get("result"),
|
|
249
|
+
target_namespace=_optional_str(value.get("target_namespace")),
|
|
238
250
|
)
|
|
239
251
|
if result.is_failure:
|
|
240
252
|
raise result.to_failure()
|
|
@@ -287,6 +299,7 @@ class NexusOperationResult:
|
|
|
287
299
|
"endpoint_name": self.endpoint_name,
|
|
288
300
|
"service_name": self.service_name,
|
|
289
301
|
"operation_name": self.operation_name,
|
|
302
|
+
"target_namespace": self.target_namespace,
|
|
290
303
|
"idempotency_key": self.request_payload.get("idempotency_key"),
|
|
291
304
|
"caller_workflow_instance_id": self.caller_workflow_instance_id,
|
|
292
305
|
"caller_workflow_run_id": self.caller_workflow_run_id,
|
|
@@ -98,10 +98,8 @@ from .workflow import (
|
|
|
98
98
|
Command,
|
|
99
99
|
FailWorkflow,
|
|
100
100
|
LocalActivityExecutionAborted,
|
|
101
|
-
NexusServiceCall,
|
|
102
101
|
PreparedLocalActivityCall,
|
|
103
102
|
RecordLocalActivity,
|
|
104
|
-
RecordSideEffect,
|
|
105
103
|
ReplayOutcome,
|
|
106
104
|
ScheduleActivity,
|
|
107
105
|
StartChildWorkflow,
|
|
@@ -166,7 +164,6 @@ _WORKFLOW_TASK_COMPLETION_AMBIGUOUS_REJECTION_REASONS = {
|
|
|
166
164
|
}
|
|
167
165
|
_WORKFLOW_TASK_COMPLETION_MAX_ATTEMPTS = 3
|
|
168
166
|
_WORKFLOW_TASK_COMPLETION_RETRY_DELAYS = (0.05, 0.2)
|
|
169
|
-
_WORKFLOW_TASK_NEXUS_RESOLUTION_LIMIT = 100
|
|
170
167
|
_LOCAL_ACTIVITY_REPORT_LIMIT = 1000
|
|
171
168
|
_WORKER_WORKFLOW_FINGERPRINTS: dict[tuple[str, str], str] = {}
|
|
172
169
|
_P = ParamSpec("_P")
|
|
@@ -1428,85 +1425,6 @@ class Worker:
|
|
|
1428
1425
|
|
|
1429
1426
|
return await handler(context)
|
|
1430
1427
|
|
|
1431
|
-
async def _resolve_workflow_nexus_commands(
|
|
1432
|
-
self,
|
|
1433
|
-
workflow_cls: type,
|
|
1434
|
-
history: list[dict[str, Any]],
|
|
1435
|
-
start_input: list[Any],
|
|
1436
|
-
task: dict[str, Any],
|
|
1437
|
-
*,
|
|
1438
|
-
payload_codec: str | None,
|
|
1439
|
-
command_codec: str,
|
|
1440
|
-
initial_commands: list[Command],
|
|
1441
|
-
) -> list[Command]:
|
|
1442
|
-
events = list(history)
|
|
1443
|
-
commands: list[Command] = list(initial_commands)
|
|
1444
|
-
recorded_commands: list[RecordSideEffect] = []
|
|
1445
|
-
|
|
1446
|
-
for _ in range(_WORKFLOW_TASK_NEXUS_RESOLUTION_LIMIT):
|
|
1447
|
-
nexus_commands = [command for command in commands if isinstance(command, NexusServiceCall)]
|
|
1448
|
-
if not nexus_commands:
|
|
1449
|
-
return recorded_commands + commands
|
|
1450
|
-
if len(nexus_commands) != len(commands):
|
|
1451
|
-
raise RuntimeError("Nexus service calls must be yielded one at a time")
|
|
1452
|
-
|
|
1453
|
-
for command in nexus_commands:
|
|
1454
|
-
result = await self.client.execute_nexus_operation(
|
|
1455
|
-
command.endpoint_name,
|
|
1456
|
-
command.service_name,
|
|
1457
|
-
command.operation_name,
|
|
1458
|
-
command.arguments,
|
|
1459
|
-
mode=command.mode,
|
|
1460
|
-
wait_for=command.wait_for,
|
|
1461
|
-
wait_timeout_seconds=command.wait_timeout_seconds,
|
|
1462
|
-
idempotency_key=command.idempotency_key,
|
|
1463
|
-
payload_codec=command.payload_codec,
|
|
1464
|
-
caller_namespace=command.caller_namespace,
|
|
1465
|
-
caller_workflow_instance_id=_string_or_none(task.get("workflow_id")),
|
|
1466
|
-
caller_workflow_run_id=_string_or_none(task.get("run_id")),
|
|
1467
|
-
service_sdk_language=command.service_sdk_language,
|
|
1468
|
-
artifact_tuple=command.artifact_tuple,
|
|
1469
|
-
published_artifact_worker_execution=command.published_artifact_worker_execution,
|
|
1470
|
-
target_workflow_instance_id=command.target_workflow_instance_id,
|
|
1471
|
-
target_workflow_run_id=command.target_workflow_run_id,
|
|
1472
|
-
connection=command.connection,
|
|
1473
|
-
queue=command.queue,
|
|
1474
|
-
business_key=command.business_key,
|
|
1475
|
-
labels=command.labels,
|
|
1476
|
-
memo=command.memo,
|
|
1477
|
-
search_attributes=command.search_attributes,
|
|
1478
|
-
duplicate_start_policy=command.duplicate_start_policy,
|
|
1479
|
-
raise_on_failure=False,
|
|
1480
|
-
)
|
|
1481
|
-
recorded_payload = result.to_recorded_payload()
|
|
1482
|
-
recorded_commands.append(RecordSideEffect(recorded_payload))
|
|
1483
|
-
events.append({
|
|
1484
|
-
"event_type": "SideEffectRecorded",
|
|
1485
|
-
"payload": {
|
|
1486
|
-
"payload_codec": command_codec,
|
|
1487
|
-
"result": serializer.encode(recorded_payload, codec=command_codec),
|
|
1488
|
-
},
|
|
1489
|
-
})
|
|
1490
|
-
|
|
1491
|
-
outcome = replay(
|
|
1492
|
-
workflow_cls,
|
|
1493
|
-
events,
|
|
1494
|
-
start_input,
|
|
1495
|
-
workflow_id=task.get("workflow_id"),
|
|
1496
|
-
run_id=_string_or_none(task.get("run_id")) or "",
|
|
1497
|
-
workflow_command_id=(
|
|
1498
|
-
_string_or_none(task.get("workflow_command_id"))
|
|
1499
|
-
or _string_or_none(task.get("task_id"))
|
|
1500
|
-
),
|
|
1501
|
-
payload_codec=payload_codec,
|
|
1502
|
-
external_storage=self.external_storage,
|
|
1503
|
-
external_storage_cache=self.external_storage_cache,
|
|
1504
|
-
cancel_requested=bool(task.get("cancel_requested", False)),
|
|
1505
|
-
)
|
|
1506
|
-
commands = list(outcome.commands)
|
|
1507
|
-
|
|
1508
|
-
raise RuntimeError("workflow yielded too many consecutive Nexus service calls")
|
|
1509
|
-
|
|
1510
1428
|
async def _renew_local_workflow_lease(self, task: dict[str, Any]) -> None:
|
|
1511
1429
|
if self._cooperative_cancellation_supported and self._local_activity_shutdown.is_set():
|
|
1512
1430
|
raise LocalActivityExecutionAborted("worker shutdown abandoned its local workflow claim")
|
|
@@ -2628,45 +2546,7 @@ class Worker:
|
|
|
2628
2546
|
log.warning("failed to report replay failure: %s", fe)
|
|
2629
2547
|
return None
|
|
2630
2548
|
|
|
2631
|
-
|
|
2632
|
-
workflow_commands = await self._resolve_workflow_nexus_commands(
|
|
2633
|
-
cls,
|
|
2634
|
-
list(history),
|
|
2635
|
-
start_input,
|
|
2636
|
-
task,
|
|
2637
|
-
payload_codec=codec,
|
|
2638
|
-
command_codec=command_codec,
|
|
2639
|
-
initial_commands=list(outcome.commands),
|
|
2640
|
-
)
|
|
2641
|
-
except Exception as e:
|
|
2642
|
-
log.exception("workflow Nexus service call resolution failed")
|
|
2643
|
-
last_local = max(
|
|
2644
|
-
(index for index, command in enumerate(outcome.commands)
|
|
2645
|
-
if isinstance(command, RecordLocalActivity) and command.outcome is not None),
|
|
2646
|
-
default=-1,
|
|
2647
|
-
)
|
|
2648
|
-
if last_local >= 0:
|
|
2649
|
-
workflow_commands = [
|
|
2650
|
-
*outcome.commands[:last_local + 1],
|
|
2651
|
-
FailWorkflow(
|
|
2652
|
-
f"Nexus resolution after local activity failed: {e}",
|
|
2653
|
-
exception_type=type(e).__name__,
|
|
2654
|
-
non_retryable=True,
|
|
2655
|
-
),
|
|
2656
|
-
]
|
|
2657
|
-
else:
|
|
2658
|
-
try:
|
|
2659
|
-
await self.client.fail_workflow_task(
|
|
2660
|
-
task_id=task_id,
|
|
2661
|
-
lease_owner=self.worker_id,
|
|
2662
|
-
workflow_task_attempt=attempt,
|
|
2663
|
-
message=f"workflow Nexus service call resolution failed: {e}",
|
|
2664
|
-
failure_type=type(e).__name__,
|
|
2665
|
-
stack_trace=traceback.format_exc(),
|
|
2666
|
-
)
|
|
2667
|
-
except Exception as fe:
|
|
2668
|
-
log.warning("failed to report workflow Nexus resolution failure: %s", fe)
|
|
2669
|
-
return None
|
|
2549
|
+
workflow_commands = list(outcome.commands)
|
|
2670
2550
|
|
|
2671
2551
|
if (
|
|
2672
2552
|
any(isinstance(command, UpsertMemo) for command in workflow_commands)
|