durable-workflow 2.11.2__tar.gz → 2.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {durable_workflow-2.11.2/src/durable_workflow.egg-info → durable_workflow-2.12.0}/PKG-INFO +1 -1
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/pyproject.toml +3 -3
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/__init__.py +2 -1
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/activity.py +61 -2
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/client.py +21 -3
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/sync.py +2 -1
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/worker.py +3 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
- durable_workflow-2.12.0/tests/test_activity_retry_info.py +82 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_client.py +35 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cooperative_remote_worker.py +20 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_prepared_local_activity_worker.py +21 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_sync.py +8 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_worker.py +39 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/LICENSE +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/README.md +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/setup.cfg +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_activity_lease.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_failure_reporting.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_metric_routes.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_poll_transport.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/telemetry.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/telemetry_carrier.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/workflow.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_activity_heartbeat_authority.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_activity_lease_authority.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_activity_lease_renewal.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_activity_lease_worker.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_client_telemetry.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_errors.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_failure_reporting.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_history_bundle_schema2.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_invocable.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_local_failure_reporting.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_metric_routes.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_metrics.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_poll_failure_backoff.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_poll_transport.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_queries.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_replay.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_replay_clock.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_replay_regression_corpus.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_schedules.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_sdk_metrics_contract.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_serializer.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_signals.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_sleep.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_sync_activities.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_telemetry.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_telemetry_carrier.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_testing_failures.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_testing_nexus.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_threaded_heartbeat_example.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_tls_trust.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_transport_diagnostics.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_updates.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_worker_deregistration.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_worker_observation.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_worker_telemetry_wire.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_workflow_entry_arguments.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_workflow_handler_validation.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_workflow_identity.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.11.2 → durable_workflow-2.12.0}/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.12.0"
|
|
8
8
|
description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -76,8 +76,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
76
76
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
77
77
|
|
|
78
78
|
[tool.durable-workflow]
|
|
79
|
-
product-train = "2.
|
|
80
|
-
registry-version = "2.
|
|
79
|
+
product-train = "2.12.0"
|
|
80
|
+
registry-version = "2.12.0"
|
|
81
81
|
supported-server-versions = "2.8.0"
|
|
82
82
|
worker-protocol-version = "1.20"
|
|
83
83
|
control-plane-version = "2"
|
|
@@ -7,7 +7,7 @@ except PackageNotFoundError: # source checkout without installed metadata
|
|
|
7
7
|
__version__ = "0.0.0+unknown"
|
|
8
8
|
|
|
9
9
|
from . import activity, sync, testing, workflow
|
|
10
|
-
from .activity import ActivityContext, ActivityInfo
|
|
10
|
+
from .activity import ActivityContext, ActivityInfo, ActivityRetryInfo
|
|
11
11
|
from .auth_composition import (
|
|
12
12
|
AUTH_COMPOSITION_CONTRACT_SCHEMA,
|
|
13
13
|
AUTH_COMPOSITION_CONTRACT_VERSION,
|
|
@@ -248,6 +248,7 @@ __all__ = [
|
|
|
248
248
|
"ActivityContext",
|
|
249
249
|
"ActivityHandler",
|
|
250
250
|
"ActivityInfo",
|
|
251
|
+
"ActivityRetryInfo",
|
|
251
252
|
"ActivityInterceptorContext",
|
|
252
253
|
"ActivityRetryPolicy",
|
|
253
254
|
"BridgeAdapterOutcome",
|
|
@@ -3,8 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import contextvars
|
|
6
|
-
|
|
7
|
-
from
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
8
|
+
from dataclasses import InitVar, dataclass, field
|
|
8
9
|
from typing import TYPE_CHECKING, Any
|
|
9
10
|
|
|
10
11
|
from ._activity_lease import current_activity_authority
|
|
@@ -20,6 +21,53 @@ _current_context: contextvars.ContextVar[ActivityContext | None] = contextvars.C
|
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
@dataclass(frozen=True, init=False)
|
|
25
|
+
class ActivityRetryInfo:
|
|
26
|
+
"""Original attempt and immutable received retry snapshot, without defaults."""
|
|
27
|
+
|
|
28
|
+
attempt_number: int
|
|
29
|
+
policy_provided: bool
|
|
30
|
+
# Immutable JSON also survives the supervised callback's spawn serialization.
|
|
31
|
+
_policy_json: str = field(repr=False)
|
|
32
|
+
|
|
33
|
+
def __init__(self, attempt_number: int, policy_provided: bool = False, policy: Any = None) -> None:
|
|
34
|
+
object.__setattr__(self, "attempt_number", attempt_number)
|
|
35
|
+
object.__setattr__(self, "policy_provided", policy_provided)
|
|
36
|
+
object.__setattr__(self, "_policy_json", json.dumps(policy))
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_task(cls, attempt_number: int, task: Mapping[str, Any]) -> ActivityRetryInfo:
|
|
40
|
+
return cls(attempt_number, "retry_policy" in task, task.get("retry_policy"))
|
|
41
|
+
|
|
42
|
+
def policy(self) -> Any:
|
|
43
|
+
"""Return a fresh copy, including unknown fields and explicit legacy null."""
|
|
44
|
+
return json.loads(self._policy_json)
|
|
45
|
+
|
|
46
|
+
def max_attempts(self) -> int | None:
|
|
47
|
+
"""A positive configured limit. An unknown limit does not mean unlimited."""
|
|
48
|
+
policy = self.policy()
|
|
49
|
+
maximum = policy.get("max_attempts") if isinstance(policy, dict) else None
|
|
50
|
+
return maximum if type(maximum) is int and maximum > 0 else None
|
|
51
|
+
|
|
52
|
+
def is_unlimited(self) -> bool:
|
|
53
|
+
"""Only an explicit max_attempts:null inside a policy means unlimited."""
|
|
54
|
+
policy = self.policy()
|
|
55
|
+
return isinstance(policy, dict) and "max_attempts" in policy and policy["max_attempts"] is None
|
|
56
|
+
|
|
57
|
+
def is_last_configured_attempt(self) -> bool | None:
|
|
58
|
+
"""Check the budget, not whether another execution will actually occur.
|
|
59
|
+
|
|
60
|
+
Cancellation, non-retryable failure, workflow closure and deadlines can
|
|
61
|
+
end execution before the configured budget is exhausted.
|
|
62
|
+
"""
|
|
63
|
+
if self.attempt_number < 1:
|
|
64
|
+
return None
|
|
65
|
+
maximum = self.max_attempts()
|
|
66
|
+
if maximum is not None:
|
|
67
|
+
return self.attempt_number >= maximum
|
|
68
|
+
return False if self.is_unlimited() else None
|
|
69
|
+
|
|
70
|
+
|
|
23
71
|
@dataclass(frozen=True)
|
|
24
72
|
class ActivityInfo:
|
|
25
73
|
"""Metadata attached to the currently running activity attempt."""
|
|
@@ -30,6 +78,17 @@ class ActivityInfo:
|
|
|
30
78
|
attempt_number: int
|
|
31
79
|
task_queue: str
|
|
32
80
|
worker_id: str
|
|
81
|
+
task_metadata: InitVar[Mapping[str, Any] | None] = None
|
|
82
|
+
retry_info: ActivityRetryInfo = field(init=False)
|
|
83
|
+
workflow_id: str | None = field(init=False)
|
|
84
|
+
run_id: str | None = field(init=False)
|
|
85
|
+
|
|
86
|
+
def __post_init__(self, task_metadata: Mapping[str, Any] | None) -> None:
|
|
87
|
+
task = task_metadata if task_metadata is not None else {}
|
|
88
|
+
object.__setattr__(self, "retry_info", ActivityRetryInfo.from_task(self.attempt_number, task))
|
|
89
|
+
for name in ("workflow_id", "run_id"):
|
|
90
|
+
value = task.get(name)
|
|
91
|
+
object.__setattr__(self, name, value if isinstance(value, str) and value else None)
|
|
33
92
|
|
|
34
93
|
|
|
35
94
|
class ActivityContext:
|
|
@@ -1386,6 +1386,10 @@ class WorkflowHandle:
|
|
|
1386
1386
|
request_id: str | None = None,
|
|
1387
1387
|
) -> dict[str, Any]:
|
|
1388
1388
|
"""Deliver an external signal to this workflow. See :meth:`Client.signal_workflow`."""
|
|
1389
|
+
if self.run_id is not None:
|
|
1390
|
+
return await self._client.signal_workflow(
|
|
1391
|
+
self.workflow_id, signal_name, args=args, request_id=request_id, run_id=self.run_id,
|
|
1392
|
+
)
|
|
1389
1393
|
return await self._client.signal_workflow(
|
|
1390
1394
|
self.workflow_id, signal_name, args=args, request_id=request_id,
|
|
1391
1395
|
)
|
|
@@ -4461,6 +4465,7 @@ class Client:
|
|
|
4461
4465
|
*,
|
|
4462
4466
|
args: list[Any] | None = None,
|
|
4463
4467
|
request_id: str | None = None,
|
|
4468
|
+
run_id: str | None = None,
|
|
4464
4469
|
) -> dict[str, Any]:
|
|
4465
4470
|
"""Deliver an external signal to a running workflow.
|
|
4466
4471
|
|
|
@@ -4469,12 +4474,18 @@ class Client:
|
|
|
4469
4474
|
observe the signal. See the main docs for how to declare the allowed
|
|
4470
4475
|
signal names on a workflow type.
|
|
4471
4476
|
|
|
4477
|
+
Pass ``run_id`` to target the original run. Omitting it targets the
|
|
4478
|
+
current run when the Server receives the request. A run-pinned
|
|
4479
|
+
:class:`WorkflowHandle` forwards its run identity automatically.
|
|
4480
|
+
|
|
4472
4481
|
``request_id`` is the caller command key (1-255 opaque, case-sensitive
|
|
4473
|
-
bytes)
|
|
4482
|
+
bytes). When omitted the client
|
|
4474
4483
|
generates ``sdk-python-signal-<uuid>`` once per call, so the transport
|
|
4475
4484
|
retry policy resends the same key. Resending the same key with the same
|
|
4476
4485
|
signal name, arguments and run target returns the original receipt
|
|
4477
|
-
|
|
4486
|
+
only when the runtime supports and applies command-key retention.
|
|
4487
|
+
A runtime without that retention can record repeated signals. Keep
|
|
4488
|
+
business-event processing idempotent and reconcile uncertain delivery.
|
|
4478
4489
|
|
|
4479
4490
|
Returns the Server's admission receipt. ``request_id`` is always
|
|
4480
4491
|
present (the key that was sent); a Server that retains command keys
|
|
@@ -4486,6 +4497,12 @@ class Client:
|
|
|
4486
4497
|
in its ``details``. Every exception raised after the key was chosen,
|
|
4487
4498
|
including transport errors, carries it as ``request_id``.
|
|
4488
4499
|
"""
|
|
4500
|
+
if run_id is not None and (not isinstance(run_id, str) or not run_id):
|
|
4501
|
+
raise ValueError("run_id must be a non-empty string when provided")
|
|
4502
|
+
path = f"/workflows/{quote(workflow_id, safe='')}"
|
|
4503
|
+
if run_id is not None:
|
|
4504
|
+
path += f"/runs/{quote(run_id, safe='')}"
|
|
4505
|
+
path += f"/signal/{quote(signal_name, safe='')}"
|
|
4489
4506
|
key = _command_request_id(request_id, "signal")
|
|
4490
4507
|
body: dict[str, Any] = {}
|
|
4491
4508
|
if args:
|
|
@@ -4493,12 +4510,13 @@ class Client:
|
|
|
4493
4510
|
args,
|
|
4494
4511
|
kind="signal",
|
|
4495
4512
|
workflow_id=workflow_id,
|
|
4513
|
+
run_id=run_id,
|
|
4496
4514
|
signal_name=signal_name,
|
|
4497
4515
|
)
|
|
4498
4516
|
body["request_id"] = key
|
|
4499
4517
|
try:
|
|
4500
4518
|
data = await self._request(
|
|
4501
|
-
"POST",
|
|
4519
|
+
"POST", path,
|
|
4502
4520
|
json=self._with_observability_context(body), context=workflow_id,
|
|
4503
4521
|
)
|
|
4504
4522
|
except Exception as exc:
|
|
@@ -535,9 +535,10 @@ class Client:
|
|
|
535
535
|
*,
|
|
536
536
|
args: list[Any] | None = None,
|
|
537
537
|
request_id: str | None = None,
|
|
538
|
+
run_id: str | None = None,
|
|
538
539
|
) -> dict[str, Any]:
|
|
539
540
|
result: dict[str, Any] = self._runner.run(
|
|
540
|
-
self._async.signal_workflow(workflow_id, signal_name, args=args, request_id=request_id)
|
|
541
|
+
self._async.signal_workflow(workflow_id, signal_name, args=args, request_id=request_id, run_id=run_id)
|
|
541
542
|
)
|
|
542
543
|
return result
|
|
543
544
|
|
|
@@ -2105,6 +2105,7 @@ class Worker:
|
|
|
2105
2105
|
handler, tuple(call.command.arguments), ActivityInfo(
|
|
2106
2106
|
task_id, call.command.activity_type, attempt.attempt_id, attempt.attempt_number,
|
|
2107
2107
|
self.task_queue, self.worker_id,
|
|
2108
|
+
task_metadata={"workflow_id": task.get("workflow_id"), "run_id": task.get("run_id")},
|
|
2108
2109
|
), {**task, "activity_attempt_id": attempt.attempt_id}, self.interceptors,
|
|
2109
2110
|
namespace=getattr(self.client, "namespace", None),
|
|
2110
2111
|
execution_context=telemetry.capture() if telemetry is not None else None,
|
|
@@ -2462,6 +2463,7 @@ class Worker:
|
|
|
2462
2463
|
attempt_number=attempt_number,
|
|
2463
2464
|
task_queue=self.task_queue,
|
|
2464
2465
|
worker_id=self.worker_id,
|
|
2466
|
+
task_metadata={"workflow_id": task.get("workflow_id"), "run_id": task.get("run_id")},
|
|
2465
2467
|
)
|
|
2466
2468
|
_set_context(ActivityContext(info=info, client=self.client, heartbeat_callback=heartbeat))
|
|
2467
2469
|
try:
|
|
@@ -3196,6 +3198,7 @@ class Worker:
|
|
|
3196
3198
|
attempt_number=attempt_number,
|
|
3197
3199
|
task_queue=self.task_queue,
|
|
3198
3200
|
worker_id=self.worker_id,
|
|
3201
|
+
task_metadata=task,
|
|
3199
3202
|
),
|
|
3200
3203
|
client=self.client,
|
|
3201
3204
|
)
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow.egg-info/SOURCES.txt
RENAMED
|
@@ -54,6 +54,7 @@ tests/test_activity_lease_authority.py
|
|
|
54
54
|
tests/test_activity_lease_renewal.py
|
|
55
55
|
tests/test_activity_lease_worker.py
|
|
56
56
|
tests/test_activity_process.py
|
|
57
|
+
tests/test_activity_retry_info.py
|
|
57
58
|
tests/test_auth_composition.py
|
|
58
59
|
tests/test_avro_value_benchmark.py
|
|
59
60
|
tests/test_avro_value_protocol.py
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pickle
|
|
4
|
+
from dataclasses import FrozenInstanceError
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from durable_workflow import ActivityInfo, ActivityRetryInfo, activity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@activity.defn(name="context.metadata")
|
|
13
|
+
def inspect_activity_context(_marker: str | None = None) -> dict[str, Any]:
|
|
14
|
+
info = activity.context().info
|
|
15
|
+
retry = info.retry_info
|
|
16
|
+
return {"workflow_id": info.workflow_id, "run_id": info.run_id, "attempt": retry.attempt_number,
|
|
17
|
+
"policy_provided": retry.policy_provided, "policy": retry.policy(), "maximum": retry.max_attempts(),
|
|
18
|
+
"unlimited": retry.is_unlimited(), "last": retry.is_last_configured_attempt()}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.parametrize(("attempt", "task", "maximum", "unlimited", "last"), [
|
|
22
|
+
(1, {"retry_policy": {"max_attempts": 3}}, 3, False, False),
|
|
23
|
+
(3, {"retry_policy": {"max_attempts": 3}}, 3, False, True),
|
|
24
|
+
(4, {"retry_policy": {"max_attempts": 3}}, 3, False, True),
|
|
25
|
+
(1, {"retry_policy": {"max_attempts": 1}}, 1, False, True),
|
|
26
|
+
(1, {}, None, False, None),
|
|
27
|
+
(1, {"retry_policy": None}, None, False, None),
|
|
28
|
+
(99, {"retry_policy": {"max_attempts": None}}, None, True, False),
|
|
29
|
+
(1, {"retry_policy": {"future": [True, 2.0]}}, None, False, None),
|
|
30
|
+
(1, {"retry_policy": {"max_attempts": "3"}}, None, False, None),
|
|
31
|
+
(1, {"retry_policy": {"max_attempts": True}}, None, False, None),
|
|
32
|
+
(1, {"retry_policy": {"max_attempts": 3.0}}, None, False, None),
|
|
33
|
+
(1, {"retry_policy": {"max_attempts": 0}}, None, False, None),
|
|
34
|
+
(1, {"retry_policy": "legacy"}, None, False, None),
|
|
35
|
+
(0, {"retry_policy": {"max_attempts": 1}}, 1, False, None),
|
|
36
|
+
])
|
|
37
|
+
def test_received_budget_preserves_unknown_and_unlimited(
|
|
38
|
+
attempt: int, task: dict[str, Any], maximum: int | None, unlimited: bool, last: bool | None,
|
|
39
|
+
) -> None:
|
|
40
|
+
retry = ActivityRetryInfo.from_task(attempt, task)
|
|
41
|
+
assert retry.attempt_number == attempt
|
|
42
|
+
assert retry.policy_provided is ("retry_policy" in task)
|
|
43
|
+
assert retry.policy() == task.get("retry_policy")
|
|
44
|
+
assert retry.max_attempts() == maximum
|
|
45
|
+
assert retry.is_unlimited() is unlimited
|
|
46
|
+
assert retry.is_last_configured_attempt() is last
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_snapshot_copies_and_original_identity_survive_spawn_serialization() -> None:
|
|
50
|
+
task = {"workflow_id": "0", "run_id": "017", "retry_policy": {"max_attempts": 3, "future": [True, 2.0, None]}}
|
|
51
|
+
info = ActivityInfo("task", "activity", "attempt", 3, "queue", "owner", task_metadata=task)
|
|
52
|
+
task["retry_policy"]["max_attempts"] = 10
|
|
53
|
+
task["workflow_id"] = "replacement"
|
|
54
|
+
copy = info.retry_info.policy()
|
|
55
|
+
copy["future"][0] = False
|
|
56
|
+
assert info.retry_info.policy() == {"max_attempts": 3, "future": [True, 2.0, None]}
|
|
57
|
+
assert info.workflow_id == "0" and info.run_id == "017"
|
|
58
|
+
assert info.retry_info.is_last_configured_attempt() is True
|
|
59
|
+
restored = pickle.loads(pickle.dumps(info))
|
|
60
|
+
assert restored == info
|
|
61
|
+
assert restored.retry_info.policy() == info.retry_info.policy()
|
|
62
|
+
with pytest.raises(FrozenInstanceError):
|
|
63
|
+
info.workflow_id = "replacement" # type: ignore[misc]
|
|
64
|
+
with pytest.raises(FrozenInstanceError):
|
|
65
|
+
info.retry_info.attempt_number = 100 # type: ignore[misc]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_legacy_constructor_has_no_invented_received_policy_or_identity() -> None:
|
|
69
|
+
info = ActivityInfo("task", "activity", "attempt", 2, "queue", "owner")
|
|
70
|
+
assert info.retry_info.attempt_number == 2
|
|
71
|
+
assert info.retry_info.policy_provided is False
|
|
72
|
+
assert info.retry_info.is_last_configured_attempt() is None
|
|
73
|
+
assert info.workflow_id is None and info.run_id is None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.parametrize("task", [
|
|
77
|
+
{}, {"workflow_id": None, "run_id": None}, {"workflow_id": "", "run_id": 42},
|
|
78
|
+
{"workflow_instance_id": "internal", "workflow_run_id": "internal-run"},
|
|
79
|
+
])
|
|
80
|
+
def test_public_claim_identity_stays_unknown_when_missing_or_invalid(task: dict[str, Any]) -> None:
|
|
81
|
+
info = ActivityInfo("task", "activity", "attempt", 1, "queue", "owner", task_metadata=task)
|
|
82
|
+
assert info.workflow_id is None and info.run_id is None
|
|
@@ -869,6 +869,41 @@ class TestWorkflowHandleControlPlane:
|
|
|
869
869
|
|
|
870
870
|
|
|
871
871
|
class TestSignalWorkflow:
|
|
872
|
+
@pytest.mark.asyncio
|
|
873
|
+
@pytest.mark.parametrize("use_handle", [False, True])
|
|
874
|
+
async def test_signal_keeps_original_run_and_business_key(self, client: Client, use_handle: bool) -> None:
|
|
875
|
+
seen = []
|
|
876
|
+
|
|
877
|
+
def receive(request: httpx.Request) -> httpx.Response:
|
|
878
|
+
seen.append(request)
|
|
879
|
+
return httpx.Response(202, json={"outcome": "received"})
|
|
880
|
+
|
|
881
|
+
await client._http.aclose()
|
|
882
|
+
client._http = httpx.AsyncClient(base_url=client.base_url, transport=httpx.MockTransport(receive))
|
|
883
|
+
async with client:
|
|
884
|
+
if use_handle:
|
|
885
|
+
handle = client.get_workflow_handle("invoice-42", run_id="original-run")
|
|
886
|
+
await handle.signal("payment_recorded", ["payment-17"], request_id="payment:17")
|
|
887
|
+
else:
|
|
888
|
+
await client.signal_workflow("invoice-42", "payment_recorded", args=["payment-17"],
|
|
889
|
+
run_id="original-run", request_id="payment:17")
|
|
890
|
+
assert len(seen) == 1
|
|
891
|
+
assert seen[0].url.path == "/api/workflows/invoice-42/runs/original-run/signal/payment_recorded"
|
|
892
|
+
body = json.loads(seen[0].content)
|
|
893
|
+
assert body["request_id"] == "payment:17"
|
|
894
|
+
assert serializer.decode_envelope(body["input"]) == ["payment-17"]
|
|
895
|
+
|
|
896
|
+
@pytest.mark.asyncio
|
|
897
|
+
@pytest.mark.parametrize("run_id", ["", False, 1])
|
|
898
|
+
async def test_invalid_run_pin_never_falls_back_to_current(self, client: Client, run_id: Any) -> None:
|
|
899
|
+
with (
|
|
900
|
+
patch.object(client._http, "request", new_callable=AsyncMock) as request,
|
|
901
|
+
pytest.raises(ValueError, match="run_id"),
|
|
902
|
+
):
|
|
903
|
+
await client.signal_workflow("invoice-42", "payment_recorded", run_id=run_id)
|
|
904
|
+
request.assert_not_awaited()
|
|
905
|
+
await client.aclose()
|
|
906
|
+
|
|
872
907
|
@pytest.mark.asyncio
|
|
873
908
|
async def test_signal(self, client: Client) -> None:
|
|
874
909
|
resp = _mock_response(200, {"ok": True})
|
|
@@ -19,6 +19,7 @@ from durable_workflow.errors import ActivityCancelled, NonRetryableError, Server
|
|
|
19
19
|
from durable_workflow.retry_policy import TransportRetryPolicy
|
|
20
20
|
from durable_workflow.worker import Worker
|
|
21
21
|
from tests.test_activity_process import wait_for_exit, wait_for_file
|
|
22
|
+
from tests.test_activity_retry_info import inspect_activity_context
|
|
22
23
|
from tests.test_worker import compatible_cluster_info
|
|
23
24
|
|
|
24
25
|
|
|
@@ -205,6 +206,25 @@ async def test_authored_heartbeat_stays_on_owner_loop_and_preserves_typed_result
|
|
|
205
206
|
client.fail_activity_task.assert_not_awaited()
|
|
206
207
|
|
|
207
208
|
|
|
209
|
+
@pytest.mark.parametrize("configured", [False, True])
|
|
210
|
+
async def test_supervised_worker_preserves_received_context(owner: Any, configured: bool) -> None:
|
|
211
|
+
worker, client = owner
|
|
212
|
+
worker.activities["remote"] = inspect_activity_context
|
|
213
|
+
work = task()
|
|
214
|
+
if configured:
|
|
215
|
+
work.update(attempt_number=3, workflow_id="0", run_id="017",
|
|
216
|
+
retry_policy={"max_attempts": 3, "future": [True, None]})
|
|
217
|
+
assert await worker._run_activity_task(work) == "completed"
|
|
218
|
+
client.fail_activity_task.assert_not_awaited()
|
|
219
|
+
assert client.complete_activity_task.await_args.kwargs["result"] == {
|
|
220
|
+
"workflow_id": "0" if configured else None, "run_id": "017" if configured else None,
|
|
221
|
+
"attempt": 3 if configured else 1, "policy_provided": configured,
|
|
222
|
+
"policy": {"max_attempts": 3, "future": [True, None]} if configured else None,
|
|
223
|
+
"maximum": 3 if configured else None, "unlimited": False, "last": True if configured else None,
|
|
224
|
+
}
|
|
225
|
+
assert not worker._remote_activity_processes
|
|
226
|
+
|
|
227
|
+
|
|
208
228
|
@pytest.mark.parametrize("field", ["lease", "heartbeat", "start_to_close", "schedule_to_close", "session", "malformed"])
|
|
209
229
|
async def test_invalid_or_elapsed_observed_bounds_prevent_callback_start(owner: Any, field: str) -> None:
|
|
210
230
|
worker, client = owner
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_prepared_local_activity_worker.py
RENAMED
|
@@ -20,6 +20,7 @@ from durable_workflow.errors import WorkflowCancelled
|
|
|
20
20
|
from durable_workflow.worker import Worker
|
|
21
21
|
from durable_workflow.workflow import LocalActivityExecutionAborted, replay
|
|
22
22
|
from tests.test_activity_process import blocked_without_python_progress, wait_for_exit, wait_for_file, wait_for_release
|
|
23
|
+
from tests.test_activity_retry_info import inspect_activity_context
|
|
23
24
|
from tests.test_cancellation_context import delivery, request, snapshot
|
|
24
25
|
from tests.test_replay_regression_corpus import PreparedLocalColdResultsWorkflow
|
|
25
26
|
from tests.test_worker import compatible_cluster_info
|
|
@@ -415,6 +416,26 @@ async def test_worker_runs_only_an_admitted_process_then_replays_canonical_resul
|
|
|
415
416
|
await wait_for_exit(int((tmp_path / "callback").read_text()))
|
|
416
417
|
|
|
417
418
|
|
|
419
|
+
async def test_prepared_local_activity_preserves_parent_context_only(
|
|
420
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path,
|
|
421
|
+
) -> None:
|
|
422
|
+
server = PreparedServer()
|
|
423
|
+
worker = await server.worker(monkeypatch, handler=inspect_activity_context)
|
|
424
|
+
task = server.task(tmp_path / "unused")
|
|
425
|
+
task["retry_policy"] = {"max_attempts": 9}
|
|
426
|
+
try:
|
|
427
|
+
commands = await worker._run_workflow_task(task)
|
|
428
|
+
assert commands is not None and [command["type"] for command in commands] == ["complete_workflow"]
|
|
429
|
+
assert serializer.decode_envelope(commands[0]["result"]) == {
|
|
430
|
+
"workflow_id": "workflow-1", "run_id": "run-1", "attempt": 1, "policy_provided": False,
|
|
431
|
+
"policy": None, "maximum": None, "unlimited": False, "last": None,
|
|
432
|
+
}
|
|
433
|
+
server.client.fail_workflow_task.assert_not_awaited()
|
|
434
|
+
assert not worker._prepared_local_activity_processes
|
|
435
|
+
finally:
|
|
436
|
+
await worker.stop()
|
|
437
|
+
|
|
438
|
+
|
|
418
439
|
async def test_bad_admission_never_spawns_and_never_completes_or_fails_a_claim(
|
|
419
440
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path,
|
|
420
441
|
) -> None:
|
|
@@ -221,6 +221,14 @@ class TestSyncClientDescribe:
|
|
|
221
221
|
|
|
222
222
|
|
|
223
223
|
class TestSyncClientSignal:
|
|
224
|
+
def test_signal_pins_the_original_run(self) -> None:
|
|
225
|
+
with Client("http://localhost:8080") as client:
|
|
226
|
+
with patch.object(client._async._http, "request", new_callable=AsyncMock,
|
|
227
|
+
return_value=_mock_response(202, {"outcome": "received"})) as request:
|
|
228
|
+
client.signal_workflow("invoice-42", "payment_recorded", run_id="original-run", request_id="payment:17")
|
|
229
|
+
assert request.call_args.args[1] == "/api/workflows/invoice-42/runs/original-run/signal/payment_recorded"
|
|
230
|
+
assert request.call_args.kwargs["json"]["request_id"] == "payment:17"
|
|
231
|
+
|
|
224
232
|
def test_signal(self) -> None:
|
|
225
233
|
client = Client("http://localhost:8080")
|
|
226
234
|
resp = _mock_response(202, {"outcome": "received"})
|
|
@@ -48,6 +48,7 @@ from durable_workflow.worker import (
|
|
|
48
48
|
_should_fail_workflow_task_after_completion_error,
|
|
49
49
|
)
|
|
50
50
|
from tests.query_task_helpers import leased_query_task
|
|
51
|
+
from tests.test_activity_retry_info import inspect_activity_context
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
class TypedCancelFlightError(Exception):
|
|
@@ -1341,6 +1342,27 @@ class TestWorkflowTaskExecution:
|
|
|
1341
1342
|
assert [command["type"] for command in replayed] == ["complete_workflow"]
|
|
1342
1343
|
mock_client.heartbeat_workflow_task.assert_not_awaited()
|
|
1343
1344
|
|
|
1345
|
+
@pytest.mark.asyncio
|
|
1346
|
+
async def test_inline_local_activity_preserves_parent_context_only(self, mock_client: AsyncMock) -> None:
|
|
1347
|
+
mock_client.heartbeat_workflow_task.return_value = {
|
|
1348
|
+
"task_id": "local-task", "lease_owner": "local-worker", "workflow_task_attempt": 1, "renewed": True,
|
|
1349
|
+
}
|
|
1350
|
+
worker = Worker(mock_client, task_queue="q1", worker_id="local-worker",
|
|
1351
|
+
workflows=[LocalWorkerWorkflow], activities=[local_echo])
|
|
1352
|
+
worker.activities["local.echo"] = inspect_activity_context
|
|
1353
|
+
commands = await worker._run_workflow_task({
|
|
1354
|
+
"task_id": "local-task", "workflow_type": "local-worker-wf", "workflow_task_attempt": 1,
|
|
1355
|
+
"workflow_id": "0", "run_id": "017", "retry_policy": {"max_attempts": 9},
|
|
1356
|
+
"history_events": [], "arguments": serializer.envelope(["ignored"]), "payload_codec": "avro",
|
|
1357
|
+
})
|
|
1358
|
+
assert commands is not None
|
|
1359
|
+
assert [command["type"] for command in commands] == ["record_local_activity", "complete_workflow"]
|
|
1360
|
+
assert serializer.decode_envelope(commands[-1]["result"]) == {
|
|
1361
|
+
"workflow_id": "0", "run_id": "017", "attempt": 1, "policy_provided": False,
|
|
1362
|
+
"policy": None, "maximum": None, "unlimited": False, "last": None,
|
|
1363
|
+
}
|
|
1364
|
+
mock_client.fail_workflow_task.assert_not_awaited()
|
|
1365
|
+
|
|
1344
1366
|
@pytest.mark.asyncio
|
|
1345
1367
|
async def test_local_activity_lease_refusal_never_completes_task(self, mock_client: AsyncMock) -> None:
|
|
1346
1368
|
mock_client.heartbeat_workflow_task.return_value = {
|
|
@@ -3316,6 +3338,23 @@ class TestUpdateValidationTaskExecution:
|
|
|
3316
3338
|
|
|
3317
3339
|
|
|
3318
3340
|
class TestActivityTaskExecution:
|
|
3341
|
+
@pytest.mark.parametrize("configured", [False, True])
|
|
3342
|
+
async def test_ordinary_worker_preserves_received_context(self, mock_client: AsyncMock, configured: bool) -> None:
|
|
3343
|
+
worker = Worker(mock_client, task_queue="q1", activities=[inspect_activity_context])
|
|
3344
|
+
metadata = {"attempt_number": 3, "workflow_id": "0", "run_id": "017",
|
|
3345
|
+
"retry_policy": {"max_attempts": 3, "future": [True, None]}} if configured else {}
|
|
3346
|
+
task = {"task_id": "metadata-task", "activity_attempt_id": "metadata-attempt",
|
|
3347
|
+
"activity_type": "context.metadata", "arguments": serializer.envelope([]), "payload_codec": "avro",
|
|
3348
|
+
**metadata}
|
|
3349
|
+
assert await worker._run_activity_task(task) == "completed"
|
|
3350
|
+
mock_client.fail_activity_task.assert_not_awaited()
|
|
3351
|
+
assert mock_client.complete_activity_task.await_args.kwargs["result"] == {
|
|
3352
|
+
"workflow_id": "0" if configured else None, "run_id": "017" if configured else None,
|
|
3353
|
+
"attempt": 3 if configured else 1, "policy_provided": configured,
|
|
3354
|
+
"policy": {"max_attempts": 3, "future": [True, None]} if configured else None,
|
|
3355
|
+
"maximum": 3 if configured else None, "unlimited": False, "last": True if configured else None,
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3319
3358
|
@pytest.mark.asyncio
|
|
3320
3359
|
async def test_sync_activity(self, mock_client: AsyncMock) -> None:
|
|
3321
3360
|
worker = Worker(mock_client, task_queue="q1", workflows=[], activities=[echo_activity])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_activity_process.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_failure_reporting.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_prepared_local_activity.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_query_task_authority.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/_sticky_workflow_cache.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/auth_composition.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/external_storage.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/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.11.2 → durable_workflow-2.12.0}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/replay_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow/telemetry_carrier.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_activity_cancellation_policies.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_activity_heartbeat_authority.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.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_remaining_time.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_scope_authoring.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_scope_boundary.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cancellation_scope_opening.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.11.2 → durable_workflow-2.12.0}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cooperative_cancellation_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_cooperative_cancellation_worker.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_docstring_cross_references.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_prepared_local_activity_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_prepared_local_activity_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
|
|
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.11.2 → durable_workflow-2.12.0}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_runtime_external_payload_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_scoped_run_cancellation_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_standalone_activity_client.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.11.2 → durable_workflow-2.12.0}/tests/test_threaded_heartbeat_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_update_signal_condition_replay.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.11.2 → durable_workflow-2.12.0}/tests/test_workflow_entrypoint_validation.py
RENAMED
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_workflow_handler_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.11.2 → durable_workflow-2.12.0}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|