durable-workflow 2.5.1__tar.gz → 2.5.3__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.1/src/durable_workflow.egg-info → durable_workflow-2.5.3}/PKG-INFO +1 -1
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/pyproject.toml +3 -3
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/__init__.py +2 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/errors.py +4 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/testing.py +2 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/worker.py +35 -15
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/workflow.py +50 -12
- {durable_workflow-2.5.1 → durable_workflow-2.5.3/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/SOURCES.txt +2 -0
- durable_workflow-2.5.3/tests/test_condition_replay_drift.py +132 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_replay_regression_corpus.py +10 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_wait_condition.py +10 -15
- durable_workflow-2.5.3/tests/test_workflow_entrypoint_validation.py +116 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/LICENSE +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/README.md +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/setup.cfg +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_client.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_errors.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_invocable.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_metrics.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_queries.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_schedules.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_serializer.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_signals.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_sleep.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_sync.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_updates.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_worker.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.3}/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.3"
|
|
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.3"
|
|
75
|
+
registry-version = "2.5.3"
|
|
76
76
|
supported-server-versions = "2.5.16"
|
|
77
77
|
worker-protocol-version = "1.19"
|
|
78
78
|
control-plane-version = "2"
|
|
@@ -81,6 +81,7 @@ from .errors import (
|
|
|
81
81
|
ExternalPayloadUnavailable,
|
|
82
82
|
ExternalPayloadUnsupported,
|
|
83
83
|
InvalidArgument,
|
|
84
|
+
InvalidWorkflowDefinition,
|
|
84
85
|
NamespaceNotFound,
|
|
85
86
|
NexusOperationFailed,
|
|
86
87
|
NonDeterministicReplayError,
|
|
@@ -369,6 +370,7 @@ __all__ = [
|
|
|
369
370
|
"NexusOperationResult",
|
|
370
371
|
"NexusServiceCall",
|
|
371
372
|
"NonDeterministicReplayError",
|
|
373
|
+
"InvalidWorkflowDefinition",
|
|
372
374
|
"NoopMetrics",
|
|
373
375
|
"QueryFailed",
|
|
374
376
|
"RuntimeCapabilityUnsupported",
|
|
@@ -27,6 +27,10 @@ class DurableWorkflowError(Exception):
|
|
|
27
27
|
"""Base class for every exception raised by the SDK."""
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
class InvalidWorkflowDefinition(DurableWorkflowError, ValueError):
|
|
31
|
+
"""Workflow authoring uses an unsupported entry point or result."""
|
|
32
|
+
|
|
33
|
+
|
|
30
34
|
class DurableOperationCancelled(DurableWorkflowError):
|
|
31
35
|
"""A durable selection member was explicitly cancelled."""
|
|
32
36
|
|
|
@@ -51,6 +51,7 @@ from .workflow import (
|
|
|
51
51
|
StartChildWorkflow,
|
|
52
52
|
StartTimer,
|
|
53
53
|
UpsertSearchAttributes,
|
|
54
|
+
_validate_workflow_entrypoint,
|
|
54
55
|
registry,
|
|
55
56
|
replay,
|
|
56
57
|
)
|
|
@@ -134,6 +135,7 @@ class WorkflowEnvironment:
|
|
|
134
135
|
f"{workflow_cls!r} is not a workflow class; "
|
|
135
136
|
"decorate it with @workflow.defn(name=...) first."
|
|
136
137
|
)
|
|
138
|
+
_validate_workflow_entrypoint(workflow_cls)
|
|
137
139
|
self._registered_workflows[name] = workflow_cls
|
|
138
140
|
|
|
139
141
|
def signal(
|
|
@@ -69,6 +69,7 @@ from .errors import (
|
|
|
69
69
|
DurableWorkflowError,
|
|
70
70
|
ExternalPayloadError,
|
|
71
71
|
InvalidArgument,
|
|
72
|
+
NonDeterministicReplayError,
|
|
72
73
|
NonRetryableError,
|
|
73
74
|
QueryFailed,
|
|
74
75
|
ServerError,
|
|
@@ -104,6 +105,7 @@ from .workflow import (
|
|
|
104
105
|
ScheduleActivity,
|
|
105
106
|
StartChildWorkflow,
|
|
106
107
|
UpsertMemo,
|
|
108
|
+
_validate_workflow_entrypoint,
|
|
107
109
|
apply_update,
|
|
108
110
|
commands_to_server_commands,
|
|
109
111
|
query_state,
|
|
@@ -1023,7 +1025,10 @@ class Worker:
|
|
|
1023
1025
|
) -> None:
|
|
1024
1026
|
self.client = client
|
|
1025
1027
|
self.task_queue = task_queue
|
|
1026
|
-
self.workflows
|
|
1028
|
+
self.workflows: dict[str, type] = {}
|
|
1029
|
+
for definition in workflows:
|
|
1030
|
+
_validate_workflow_entrypoint(definition)
|
|
1031
|
+
self.workflows[_workflow_name(definition)] = definition
|
|
1027
1032
|
self.workflow_definition_fingerprints = {
|
|
1028
1033
|
workflow_type: _workflow_definition_fingerprint(workflow_cls)
|
|
1029
1034
|
for workflow_type, workflow_cls in self.workflows.items()
|
|
@@ -2498,20 +2503,35 @@ class Worker:
|
|
|
2498
2503
|
|
|
2499
2504
|
update_id = task.get("workflow_update_id")
|
|
2500
2505
|
if isinstance(update_id, str) and update_id:
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2506
|
+
try:
|
|
2507
|
+
update_command = apply_update(
|
|
2508
|
+
cls,
|
|
2509
|
+
history,
|
|
2510
|
+
start_input,
|
|
2511
|
+
update_id,
|
|
2512
|
+
workflow_id=task.get("workflow_id"),
|
|
2513
|
+
run_id=run_id,
|
|
2514
|
+
payload_codec=codec,
|
|
2515
|
+
external_storage=self.external_storage,
|
|
2516
|
+
external_storage_cache=self.external_storage_cache,
|
|
2517
|
+
prepare_local_activities=self._prepared_local_activities_supported,
|
|
2518
|
+
prepare_local_activity_groups=self._prepared_local_activity_groups_supported,
|
|
2519
|
+
local_activity_cancellation_policies=self._local_activity_cancellation_policies,
|
|
2520
|
+
)
|
|
2521
|
+
except NonDeterministicReplayError as exc:
|
|
2522
|
+
log.exception("workflow update task %s replay failed for run %s", task_id, run_id)
|
|
2523
|
+
try:
|
|
2524
|
+
await self.client.fail_workflow_task(
|
|
2525
|
+
task_id=task_id,
|
|
2526
|
+
lease_owner=self.worker_id,
|
|
2527
|
+
workflow_task_attempt=attempt,
|
|
2528
|
+
message=f"workflow replay failed before update {update_id!r}: {exc}",
|
|
2529
|
+
failure_type=type(exc).__name__,
|
|
2530
|
+
stack_trace=traceback.format_exc(),
|
|
2531
|
+
)
|
|
2532
|
+
except Exception as failure_error:
|
|
2533
|
+
log.warning("failed to report update replay failure: %s", failure_error)
|
|
2534
|
+
return None
|
|
2515
2535
|
command = update_command.to_server_command(
|
|
2516
2536
|
self.task_queue,
|
|
2517
2537
|
payload_codec=command_codec,
|
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
import base64
|
|
22
22
|
import contextlib
|
|
23
23
|
import hashlib
|
|
24
|
+
import inspect
|
|
24
25
|
import logging
|
|
25
26
|
import math
|
|
26
27
|
import random
|
|
@@ -58,6 +59,7 @@ from .errors import (
|
|
|
58
59
|
ChildWorkflowFailed,
|
|
59
60
|
ChildWorkflowTerminated,
|
|
60
61
|
DurableOperationCancelled,
|
|
62
|
+
InvalidWorkflowDefinition,
|
|
61
63
|
NexusOperationFailed,
|
|
62
64
|
NonDeterministicReplayError,
|
|
63
65
|
QueryFailed,
|
|
@@ -97,6 +99,31 @@ MESSAGE_STREAM_CURSOR_SCHEMA = "durable-workflow.v2.message-stream.cursor"
|
|
|
97
99
|
MESSAGE_STREAM_MAX_BATCH = 100
|
|
98
100
|
|
|
99
101
|
|
|
102
|
+
def _validate_workflow_entrypoint(cls: type) -> None:
|
|
103
|
+
entrypoint = getattr(cls, "run", None)
|
|
104
|
+
if not callable(entrypoint):
|
|
105
|
+
return
|
|
106
|
+
targets = [inspect.unwrap(entrypoint)]
|
|
107
|
+
if not inspect.isroutine(targets[0]):
|
|
108
|
+
targets.append(inspect.unwrap(targets[0].__call__))
|
|
109
|
+
if any(inspect.iscoroutinefunction(target) or inspect.isasyncgenfunction(target) for target in targets):
|
|
110
|
+
name = getattr(cls, "__workflow_name__", cls.__name__)
|
|
111
|
+
raise InvalidWorkflowDefinition(
|
|
112
|
+
f"unsupported_workflow_entrypoint: workflow {name!r} ({cls.__qualname__}.run) "
|
|
113
|
+
"must use a synchronous function or generator; yield durable commands instead of async def."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _validate_workflow_result(value: Any, cls: type) -> None:
|
|
118
|
+
if inspect.isawaitable(value) or inspect.isasyncgen(value):
|
|
119
|
+
if inspect.iscoroutine(value):
|
|
120
|
+
value.close()
|
|
121
|
+
raise InvalidWorkflowDefinition(
|
|
122
|
+
f"unsupported_workflow_result: {cls.__qualname__}.run returned an async value; "
|
|
123
|
+
"use a synchronous result or yield durable commands."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
100
127
|
def defn(*, name: str) -> Callable[[type[_WorkflowT]], type[_WorkflowT]]:
|
|
101
128
|
"""Register a class as a workflow type under a language-neutral name.
|
|
102
129
|
|
|
@@ -108,6 +135,7 @@ def defn(*, name: str) -> Callable[[type[_WorkflowT]], type[_WorkflowT]]:
|
|
|
108
135
|
|
|
109
136
|
def wrap(cls: type[_WorkflowT]) -> type[_WorkflowT]:
|
|
110
137
|
cls.__workflow_name__ = name # type: ignore[attr-defined]
|
|
138
|
+
_validate_workflow_entrypoint(cls)
|
|
111
139
|
signals: dict[str, str] = {}
|
|
112
140
|
queries: dict[str, str] = {}
|
|
113
141
|
updates: dict[str, str] = {}
|
|
@@ -3039,6 +3067,8 @@ def apply_update(
|
|
|
3039
3067
|
prepare_local_activity_groups=prepare_local_activity_groups,
|
|
3040
3068
|
local_activity_cancellation_policies=local_activity_cancellation_policies,
|
|
3041
3069
|
)
|
|
3070
|
+
except NonDeterministicReplayError:
|
|
3071
|
+
raise
|
|
3042
3072
|
except Exception as exc:
|
|
3043
3073
|
return _fail_update_from_exception(
|
|
3044
3074
|
update_id,
|
|
@@ -4037,6 +4067,7 @@ def _replay_state(
|
|
|
4037
4067
|
allow_cancellation_scope_authoring: bool = False,
|
|
4038
4068
|
allow_cancellation_scope_delivery: bool = False,
|
|
4039
4069
|
) -> _ReplayState:
|
|
4070
|
+
_validate_workflow_entrypoint(workflow_cls)
|
|
4040
4071
|
if allow_cancellation_scope_delivery and not allow_cancellation_scope_authoring:
|
|
4041
4072
|
raise LocalActivityExecutionAborted("scope delivery requires canonical scope authoring")
|
|
4042
4073
|
if prepare_local_activity_groups and not prepare_local_activities:
|
|
@@ -5115,16 +5146,20 @@ def _replay_state(
|
|
|
5115
5146
|
for _ in range(boundary):
|
|
5116
5147
|
_apply_receiver(pending_receivers.pop(0))
|
|
5117
5148
|
|
|
5118
|
-
def _condition_wait_mismatch(
|
|
5149
|
+
def _condition_wait_mismatch(
|
|
5150
|
+
opened: Mapping[str, Any], cmd: WaitCondition,
|
|
5151
|
+
) -> NonDeterministicReplayError | None:
|
|
5152
|
+
step = recorded_wait_steps[wait_yield_count]
|
|
5153
|
+
identity = f"condition wait {opened.get('condition_wait_id')!r}"
|
|
5119
5154
|
opened_key = opened.get("condition_key")
|
|
5120
5155
|
if isinstance(opened_key, str) and opened_key != (cmd.condition_key or ""):
|
|
5121
|
-
return
|
|
5122
|
-
|
|
5123
|
-
|
|
5156
|
+
return NonDeterministicReplayError(
|
|
5157
|
+
step.workflow_sequence, "condition wait", step.event_types,
|
|
5158
|
+
detail=(
|
|
5159
|
+
f"condition_key_changed: {identity} wait_condition key changed during replay: "
|
|
5124
5160
|
f"history has {opened_key!r}, workflow yielded "
|
|
5125
5161
|
f"{cmd.condition_key!r}"
|
|
5126
5162
|
),
|
|
5127
|
-
exception_type="NonDeterministicWaitCondition",
|
|
5128
5163
|
)
|
|
5129
5164
|
|
|
5130
5165
|
opened_fingerprint = opened.get("condition_definition_fingerprint")
|
|
@@ -5132,13 +5167,14 @@ def _replay_state(
|
|
|
5132
5167
|
isinstance(opened_fingerprint, str)
|
|
5133
5168
|
and cmd.condition_definition_fingerprint != opened_fingerprint
|
|
5134
5169
|
):
|
|
5135
|
-
return
|
|
5136
|
-
|
|
5170
|
+
return NonDeterministicReplayError(
|
|
5171
|
+
step.workflow_sequence, "condition wait", step.event_types,
|
|
5172
|
+
detail=(
|
|
5173
|
+
f"condition_fingerprint_changed: {identity} "
|
|
5137
5174
|
"wait_condition predicate fingerprint changed during replay: "
|
|
5138
5175
|
f"history has {opened_fingerprint!r}, workflow yielded "
|
|
5139
5176
|
f"{cmd.condition_definition_fingerprint!r}"
|
|
5140
5177
|
),
|
|
5141
|
-
exception_type="NonDeterministicWaitCondition",
|
|
5142
5178
|
)
|
|
5143
5179
|
|
|
5144
5180
|
return None
|
|
@@ -5172,6 +5208,7 @@ def _replay_state(
|
|
|
5172
5208
|
except Exception as exc:
|
|
5173
5209
|
return _state([_fail_workflow_from_exception(exc)])
|
|
5174
5210
|
if not hasattr(gen, "__next__"):
|
|
5211
|
+
_validate_workflow_result(gen, workflow_cls)
|
|
5175
5212
|
if isinstance(gen, ContinueAsNew):
|
|
5176
5213
|
_assert_no_unconsumed_history("continue as new")
|
|
5177
5214
|
return _state([gen])
|
|
@@ -6032,6 +6069,7 @@ def _replay_state(
|
|
|
6032
6069
|
return next(iter(memberships))
|
|
6033
6070
|
|
|
6034
6071
|
def _terminal_state(value: Any, *, include_pending: bool) -> _ReplayState:
|
|
6072
|
+
_validate_workflow_result(value, workflow_cls)
|
|
6035
6073
|
_apply_due_receivers()
|
|
6036
6074
|
_consume_terminal_condition_reopens()
|
|
6037
6075
|
commands = list(pending) if include_pending else []
|
|
@@ -6624,7 +6662,7 @@ def _replay_state(
|
|
|
6624
6662
|
next_value = True
|
|
6625
6663
|
continue
|
|
6626
6664
|
if next_opened_mismatch is not None:
|
|
6627
|
-
|
|
6665
|
+
raise next_opened_mismatch
|
|
6628
6666
|
step = _next_unconsumed_recorded_step()
|
|
6629
6667
|
if step is not None:
|
|
6630
6668
|
_assert_step_matches(cmd, step)
|
|
@@ -6676,7 +6714,7 @@ def _replay_state(
|
|
|
6676
6714
|
opened = wait_opened[wait_yield_count]
|
|
6677
6715
|
mismatch = _condition_wait_mismatch(opened, cmd)
|
|
6678
6716
|
if mismatch is not None:
|
|
6679
|
-
|
|
6717
|
+
raise mismatch
|
|
6680
6718
|
opened_id = opened.get("condition_wait_id")
|
|
6681
6719
|
if isinstance(opened_id, str):
|
|
6682
6720
|
resolution = wait_resolutions.get(opened_id)
|
|
@@ -6784,10 +6822,10 @@ def _replay_state(
|
|
|
6784
6822
|
try:
|
|
6785
6823
|
return _terminal_state(stop.value, include_pending=True)
|
|
6786
6824
|
except Exception as exc:
|
|
6787
|
-
if isinstance(exc, NonDeterministicReplayError):
|
|
6825
|
+
if isinstance(exc, (NonDeterministicReplayError, InvalidWorkflowDefinition)):
|
|
6788
6826
|
raise
|
|
6789
6827
|
return _state(pending + [_fail_workflow_from_exception(exc)])
|
|
6790
|
-
except NonDeterministicReplayError:
|
|
6828
|
+
except (NonDeterministicReplayError, InvalidWorkflowDefinition):
|
|
6791
6829
|
raise
|
|
6792
6830
|
except LocalActivityExecutionAborted:
|
|
6793
6831
|
raise
|
|
@@ -62,6 +62,7 @@ tests/test_committed_cancellation_scope_descendants.py
|
|
|
62
62
|
tests/test_committed_cancellation_scope_groups.py
|
|
63
63
|
tests/test_committed_cancellation_scope_history.py
|
|
64
64
|
tests/test_committed_cancellation_scope_replay.py
|
|
65
|
+
tests/test_condition_replay_drift.py
|
|
65
66
|
tests/test_control_plane_parity_fixtures.py
|
|
66
67
|
tests/test_cooperative_cancellation.py
|
|
67
68
|
tests/test_cooperative_cancellation_client.py
|
|
@@ -117,6 +118,7 @@ tests/test_updates.py
|
|
|
117
118
|
tests/test_wait_condition.py
|
|
118
119
|
tests/test_worker.py
|
|
119
120
|
tests/test_worker_session.py
|
|
121
|
+
tests/test_workflow_entrypoint_validation.py
|
|
120
122
|
tests/test_workflow_result_timeout.py
|
|
121
123
|
tests/test_workflow_streams.py
|
|
122
124
|
tests/test_workflow_updates_conformance.py
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from durable_workflow import Worker, serializer, workflow
|
|
8
|
+
from durable_workflow.client import Client
|
|
9
|
+
from durable_workflow.errors import NonDeterministicReplayError, QueryFailed
|
|
10
|
+
from durable_workflow.workflow import WorkflowContext, apply_update, query_state, replay
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@workflow.defn(name="tests.replay.condition-identity")
|
|
14
|
+
class ConditionIdentityWorkflow:
|
|
15
|
+
@workflow.query("state")
|
|
16
|
+
def state(self) -> str:
|
|
17
|
+
return "waiting"
|
|
18
|
+
|
|
19
|
+
@workflow.update("approve")
|
|
20
|
+
def approve(self) -> str:
|
|
21
|
+
return "approved"
|
|
22
|
+
|
|
23
|
+
def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
|
|
24
|
+
yield ctx.wait_condition(lambda: False, key="approved")
|
|
25
|
+
return "done"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def condition_history(field: str) -> list[dict]:
|
|
29
|
+
return [{
|
|
30
|
+
"event_type": "ConditionWaitOpened",
|
|
31
|
+
"payload": {
|
|
32
|
+
"sequence": 7,
|
|
33
|
+
"condition_wait_id": "wait-original",
|
|
34
|
+
"condition_key": "approved",
|
|
35
|
+
field: "old-key" if field == "condition_key" else "sha256:different",
|
|
36
|
+
},
|
|
37
|
+
}]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("field", ["condition_key", "condition_definition_fingerprint"])
|
|
41
|
+
@pytest.mark.parametrize("consumer", ["replay", "query", "update"])
|
|
42
|
+
def test_condition_drift_is_not_a_business_failure(field: str, consumer: str) -> None:
|
|
43
|
+
history = condition_history(field)
|
|
44
|
+
error = QueryFailed if consumer == "query" else NonDeterministicReplayError
|
|
45
|
+
with pytest.raises(error) as captured:
|
|
46
|
+
if consumer == "query":
|
|
47
|
+
query_state(ConditionIdentityWorkflow, history, [], "state")
|
|
48
|
+
elif consumer == "update":
|
|
49
|
+
apply_update(ConditionIdentityWorkflow, history, [], "update-original")
|
|
50
|
+
else:
|
|
51
|
+
replay(ConditionIdentityWorkflow, history, [])
|
|
52
|
+
|
|
53
|
+
mismatch = captured.value.__cause__ if consumer == "query" else captured.value
|
|
54
|
+
assert isinstance(mismatch, NonDeterministicReplayError)
|
|
55
|
+
assert mismatch.workflow_sequence == 7
|
|
56
|
+
assert "wait-original" in str(mismatch)
|
|
57
|
+
assert "changed" in str(mismatch)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
@pytest.mark.parametrize("field", ["condition_key", "condition_definition_fingerprint"])
|
|
62
|
+
@pytest.mark.parametrize("consumer", ["workflow", "update"])
|
|
63
|
+
async def test_worker_reports_drift_once_without_a_completion(field: str, consumer: str) -> None:
|
|
64
|
+
client = AsyncMock(spec=Client)
|
|
65
|
+
worker = Worker(client, task_queue="condition-drift", workflows=[ConditionIdentityWorkflow])
|
|
66
|
+
task = {
|
|
67
|
+
"task_id": "task-original",
|
|
68
|
+
"workflow_id": "workflow-original",
|
|
69
|
+
"run_id": "run-original",
|
|
70
|
+
"workflow_type": "tests.replay.condition-identity",
|
|
71
|
+
"workflow_task_attempt": 2,
|
|
72
|
+
"history_events": condition_history(field),
|
|
73
|
+
"arguments": serializer.envelope([], codec="avro"),
|
|
74
|
+
"payload_codec": "avro",
|
|
75
|
+
}
|
|
76
|
+
if consumer == "update":
|
|
77
|
+
task["workflow_update_id"] = "update-original"
|
|
78
|
+
|
|
79
|
+
assert await worker._run_workflow_task_core(task) is None
|
|
80
|
+
|
|
81
|
+
client.complete_workflow_task.assert_not_awaited()
|
|
82
|
+
client.fail_workflow_task.assert_awaited_once()
|
|
83
|
+
report = client.fail_workflow_task.await_args.kwargs
|
|
84
|
+
assert report["task_id"] == "task-original"
|
|
85
|
+
assert report["workflow_task_attempt"] == 2
|
|
86
|
+
assert report["lease_owner"] == worker.worker_id
|
|
87
|
+
assert report["failure_type"] == "NonDeterministicReplayError"
|
|
88
|
+
assert "wait-original" in report["message"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_application_predicate_failure_remains_terminal() -> None:
|
|
92
|
+
@workflow.defn(name="tests.condition-predicate-failure")
|
|
93
|
+
class PredicateFailure:
|
|
94
|
+
def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
|
|
95
|
+
def predicate() -> bool:
|
|
96
|
+
raise ValueError("application failed")
|
|
97
|
+
|
|
98
|
+
yield ctx.wait_condition(predicate, key="approved")
|
|
99
|
+
|
|
100
|
+
outcome = replay(PredicateFailure, [], [])
|
|
101
|
+
assert len(outcome.commands) == 1
|
|
102
|
+
command = outcome.commands[0].to_server_command("condition-drift")
|
|
103
|
+
assert command["type"] == "fail_workflow"
|
|
104
|
+
assert command["exception_type"] == "ValueError"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.asyncio
|
|
108
|
+
@pytest.mark.parametrize("field", ["condition_key", "condition_definition_fingerprint"])
|
|
109
|
+
async def test_query_worker_reports_unavailable_state_without_changing_the_run(field: str) -> None:
|
|
110
|
+
client = AsyncMock(spec=Client)
|
|
111
|
+
worker = Worker(client, task_queue="condition-drift", workflows=[ConditionIdentityWorkflow])
|
|
112
|
+
outcome = await worker._run_query_task({
|
|
113
|
+
"query_task_id": "query-original",
|
|
114
|
+
"query_task_attempt": 2,
|
|
115
|
+
"workflow_id": "workflow-original",
|
|
116
|
+
"run_id": "run-original",
|
|
117
|
+
"workflow_type": "tests.replay.condition-identity",
|
|
118
|
+
"query_name": "state",
|
|
119
|
+
"history_events": condition_history(field),
|
|
120
|
+
"workflow_arguments": serializer.envelope([], codec="avro"),
|
|
121
|
+
"query_arguments": serializer.envelope([], codec="avro"),
|
|
122
|
+
"payload_codec": "avro",
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
assert outcome == "failed"
|
|
126
|
+
client.complete_query_task.assert_not_awaited()
|
|
127
|
+
client.complete_workflow_task.assert_not_awaited()
|
|
128
|
+
client.fail_workflow_task.assert_not_awaited()
|
|
129
|
+
client.fail_query_task.assert_awaited_once()
|
|
130
|
+
report = client.fail_query_task.await_args.kwargs
|
|
131
|
+
assert report["reason"] == "query_workflow_state_unavailable"
|
|
132
|
+
assert "wait-original" in report["message"]
|
|
@@ -17,6 +17,7 @@ from durable_workflow.workflow import (
|
|
|
17
17
|
commands_to_server_commands,
|
|
18
18
|
query_state,
|
|
19
19
|
)
|
|
20
|
+
from tests.test_condition_replay_drift import ConditionIdentityWorkflow
|
|
20
21
|
from tests.test_golden_history_replay import (
|
|
21
22
|
GoldenSagaCompensationWorkflow,
|
|
22
23
|
GoldenSignalWaitWorkflow,
|
|
@@ -251,7 +252,15 @@ class ChildPolicyAuthorWorkflow:
|
|
|
251
252
|
return (yield ctx.start_child_workflow("child", []))
|
|
252
253
|
|
|
253
254
|
|
|
255
|
+
class UnsupportedAsyncWorkflow:
|
|
256
|
+
__workflow_name__ = "tests.replay.unsupported-async"
|
|
257
|
+
|
|
258
|
+
async def run(self, ctx: WorkflowContext) -> str:
|
|
259
|
+
return "body was never executed"
|
|
260
|
+
|
|
261
|
+
|
|
254
262
|
WORKFLOWS = [
|
|
263
|
+
UnsupportedAsyncWorkflow,
|
|
255
264
|
ChildPolicyAuthorWorkflow,
|
|
256
265
|
ColdReplacementSatisfiedConditionWorkflow,
|
|
257
266
|
CooperativeReopenedConditionCleanupWorkflow,
|
|
@@ -283,6 +292,7 @@ WORKFLOWS = [
|
|
|
283
292
|
YieldedContinueAfterMetadataWorkflow,
|
|
284
293
|
]
|
|
285
294
|
WORKFLOW_TYPES = {str(getattr(workflow, "__workflow_name__", workflow.__name__)): workflow for workflow in WORKFLOWS}
|
|
295
|
+
WORKFLOW_TYPES["tests.replay.condition-identity"] = ConditionIdentityWorkflow
|
|
286
296
|
|
|
287
297
|
|
|
288
298
|
def _fixture_paths() -> list[Path]:
|
|
@@ -6,7 +6,6 @@ from durable_workflow import serializer, workflow
|
|
|
6
6
|
from durable_workflow.errors import NonDeterministicReplayError
|
|
7
7
|
from durable_workflow.workflow import (
|
|
8
8
|
CompleteWorkflow,
|
|
9
|
-
FailWorkflow,
|
|
10
9
|
WaitCondition,
|
|
11
10
|
WorkflowContext,
|
|
12
11
|
query_state,
|
|
@@ -1052,14 +1051,12 @@ class TestReplayWaitCondition:
|
|
|
1052
1051
|
},
|
|
1053
1052
|
]
|
|
1054
1053
|
|
|
1055
|
-
|
|
1054
|
+
with pytest.raises(NonDeterministicReplayError, match="key changed") as captured:
|
|
1055
|
+
replay(WaitUntilApproved, history, [])
|
|
1056
1056
|
|
|
1057
|
-
assert
|
|
1058
|
-
assert
|
|
1059
|
-
|
|
1060
|
-
assert isinstance(cmd, FailWorkflow)
|
|
1061
|
-
assert cmd.exception_type == "NonDeterministicWaitCondition"
|
|
1062
|
-
assert "key changed" in cmd.message
|
|
1057
|
+
assert captured.value.workflow_sequence == 1
|
|
1058
|
+
assert "old-key" in str(captured.value)
|
|
1059
|
+
assert "approved" in str(captured.value)
|
|
1063
1060
|
|
|
1064
1061
|
def test_replay_rejects_changed_condition_fingerprint(self) -> None:
|
|
1065
1062
|
initial = replay(WaitUntilApproved, [], [])
|
|
@@ -1077,14 +1074,12 @@ class TestReplayWaitCondition:
|
|
|
1077
1074
|
},
|
|
1078
1075
|
]
|
|
1079
1076
|
|
|
1080
|
-
|
|
1077
|
+
with pytest.raises(NonDeterministicReplayError, match="predicate fingerprint changed") as captured:
|
|
1078
|
+
replay(WaitUntilApproved, history, [])
|
|
1081
1079
|
|
|
1082
|
-
assert
|
|
1083
|
-
|
|
1084
|
-
assert
|
|
1085
|
-
assert cmd.exception_type == "NonDeterministicWaitCondition"
|
|
1086
|
-
assert "predicate fingerprint changed" in cmd.message
|
|
1087
|
-
assert fingerprint in cmd.message
|
|
1080
|
+
assert captured.value.workflow_sequence == 1
|
|
1081
|
+
assert "sha256:different" in str(captured.value)
|
|
1082
|
+
assert fingerprint in str(captured.value)
|
|
1088
1083
|
|
|
1089
1084
|
def test_replay_accepts_matching_condition_fingerprint(self) -> None:
|
|
1090
1085
|
initial = replay(WaitUntilApproved, [], [])
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Generator
|
|
4
|
+
from functools import partial, wraps
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from durable_workflow import Client, Worker, workflow
|
|
10
|
+
from durable_workflow.testing import WorkflowEnvironment
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CoroutineWorkflow:
|
|
14
|
+
async def run(self, ctx: Any) -> str:
|
|
15
|
+
return "never executed"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AsyncGeneratorWorkflow:
|
|
19
|
+
async def run(self, ctx: Any) -> AsyncIterator[str]:
|
|
20
|
+
yield "unsupported"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncCallable:
|
|
24
|
+
async def __call__(self, ctx: Any) -> str:
|
|
25
|
+
return "unsupported"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CallableWorkflow:
|
|
29
|
+
run = AsyncCallable()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PartialWorkflow:
|
|
33
|
+
run = staticmethod(partial(CoroutineWorkflow.run, object()))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ASYNC_DEFINITIONS = [CoroutineWorkflow, AsyncGeneratorWorkflow, CallableWorkflow, PartialWorkflow]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize("definition", ASYNC_DEFINITIONS)
|
|
40
|
+
def test_definition_rejects_async_entrypoints(definition: type) -> None:
|
|
41
|
+
with pytest.raises(ValueError, match="unsupported_workflow_entrypoint.*synchronous"):
|
|
42
|
+
workflow.defn(name="invalid-async")(definition)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.parametrize("definition", ASYNC_DEFINITIONS)
|
|
46
|
+
def test_worker_rejects_undecorated_async_entrypoints_before_network(definition: type) -> None:
|
|
47
|
+
with pytest.raises(ValueError, match=definition.__name__):
|
|
48
|
+
Worker(Client("http://127.0.0.1:1"), task_queue="unused", workflows=[definition])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.parametrize("definition", ASYNC_DEFINITIONS)
|
|
52
|
+
def test_direct_replay_rejects_async_without_creating_a_coroutine(definition: type) -> None:
|
|
53
|
+
with pytest.raises(ValueError, match="unsupported_workflow_entrypoint"):
|
|
54
|
+
workflow.replay(definition, [], [])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_test_environment_revalidates_entrypoint_after_decoration() -> None:
|
|
58
|
+
@workflow.defn(name="changed-entrypoint")
|
|
59
|
+
class ChangedWorkflow:
|
|
60
|
+
def run(self, ctx: Any) -> str:
|
|
61
|
+
return "valid"
|
|
62
|
+
|
|
63
|
+
ChangedWorkflow.run = CoroutineWorkflow.run # type: ignore[assignment]
|
|
64
|
+
with pytest.raises(ValueError, match="changed-entrypoint"):
|
|
65
|
+
WorkflowEnvironment().register_workflow(ChangedWorkflow)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_wrapped_and_inherited_async_entrypoints_are_rejected() -> None:
|
|
69
|
+
class InheritedWorkflow(CoroutineWorkflow):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
class WrappedWorkflow:
|
|
73
|
+
@wraps(CoroutineWorkflow.run)
|
|
74
|
+
def run(self, ctx: Any) -> Any:
|
|
75
|
+
return CoroutineWorkflow.run(self, ctx)
|
|
76
|
+
|
|
77
|
+
for definition in [InheritedWorkflow, WrappedWorkflow]:
|
|
78
|
+
with pytest.raises(ValueError, match="unsupported_workflow_entrypoint"):
|
|
79
|
+
workflow.replay(definition, [], [])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.parametrize("generator", [False, True])
|
|
83
|
+
def test_runtime_awaitable_result_is_not_completed(generator: bool) -> None:
|
|
84
|
+
class AwaitableResultWorkflow:
|
|
85
|
+
def run(self, ctx: Any) -> Any:
|
|
86
|
+
return CoroutineWorkflow.run(self, ctx)
|
|
87
|
+
|
|
88
|
+
class GeneratorAwaitableResultWorkflow:
|
|
89
|
+
def run(self, ctx: Any) -> Generator[Any, Any, Any]:
|
|
90
|
+
if False:
|
|
91
|
+
yield None
|
|
92
|
+
return CoroutineWorkflow.run(self, ctx)
|
|
93
|
+
|
|
94
|
+
definition = GeneratorAwaitableResultWorkflow if generator else AwaitableResultWorkflow
|
|
95
|
+
with pytest.raises(ValueError, match="unsupported_workflow_result"):
|
|
96
|
+
workflow.replay(definition, [], [])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_synchronous_and_generator_workflows_remain_supported() -> None:
|
|
100
|
+
@workflow.defn(name="valid-sync")
|
|
101
|
+
class SynchronousWorkflow:
|
|
102
|
+
def run(self, ctx: Any) -> str:
|
|
103
|
+
return "done"
|
|
104
|
+
|
|
105
|
+
@workflow.defn(name="valid-generator")
|
|
106
|
+
class GeneratorWorkflow:
|
|
107
|
+
def run(self, ctx: Any) -> Generator[Any, Any, str]:
|
|
108
|
+
if False:
|
|
109
|
+
yield None
|
|
110
|
+
return "done"
|
|
111
|
+
|
|
112
|
+
Worker(Client("http://127.0.0.1:1"), task_queue="unused", workflows=[SynchronousWorkflow, GeneratorWorkflow])
|
|
113
|
+
environment = WorkflowEnvironment()
|
|
114
|
+
for definition in [SynchronousWorkflow, GeneratorWorkflow]:
|
|
115
|
+
assert environment.execute_workflow(definition) == "done"
|
|
116
|
+
assert workflow.replay(definition, [], []).commands[0].result == "done" # type: ignore[union-attr]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/_cooperative_cancellation.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/_prepared_local_activity.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/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.1 → durable_workflow-2.5.3}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/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.1 → durable_workflow-2.5.3}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/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
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/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.1 → durable_workflow-2.5.3}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/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.1 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_history.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_committed_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_cooperative_cancellation_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/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
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_policies.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_prepared_local_activity_worker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_runtime_external_payload_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/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.1 → durable_workflow-2.5.3}/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
|
{durable_workflow-2.5.1 → durable_workflow-2.5.3}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|