durable-workflow 2.12.0__tar.gz → 2.12.2__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.12.0/src/durable_workflow.egg-info → durable_workflow-2.12.2}/PKG-INFO +13 -1
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/README.md +9 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/pyproject.toml +6 -3
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/workflow.py +222 -38
- {durable_workflow-2.12.0 → durable_workflow-2.12.2/src/durable_workflow.egg-info}/PKG-INFO +13 -1
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
- durable_workflow-2.12.2/tests/test_generator_lifecycle.py +401 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_replay_regression_corpus.py +26 -7
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_wait_condition.py +41 -1
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/LICENSE +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/setup.cfg +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_activity_lease.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_failure_reporting.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_metric_routes.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_poll_transport.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/telemetry.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/telemetry_carrier.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/worker.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_heartbeat_authority.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_lease_authority.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_lease_renewal.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_lease_worker.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_activity_retry_info.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_client.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_client_telemetry.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_errors.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_failure_reporting.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_history_bundle_schema2.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_invocable.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_local_failure_reporting.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_metric_routes.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_metrics.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_poll_failure_backoff.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_poll_transport.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_queries.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_replay.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_replay_clock.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_schedules.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_sdk_metrics_contract.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_serializer.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_signals.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_sleep.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_sync.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_sync_activities.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_telemetry.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_telemetry_carrier.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_testing_failures.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_testing_nexus.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_threaded_heartbeat_example.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_tls_trust.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_transport_diagnostics.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_updates.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_worker.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_worker_deregistration.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_worker_observation.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_worker_telemetry_wire.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_entry_arguments.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_handler_validation.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_identity.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.12.0 → durable_workflow-2.12.2}/tests/test_workflow_updates_conformance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durable-workflow
|
|
3
|
-
Version: 2.12.
|
|
3
|
+
Version: 2.12.2
|
|
4
4
|
Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
|
|
5
5
|
Author: Durable Workflow Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -14,6 +14,9 @@ Classifier: Programming Language :: Python :: 3
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
17
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
21
|
Classifier: Typing :: Typed
|
|
19
22
|
Requires-Python: >=3.10
|
|
@@ -186,6 +189,15 @@ independently of application heartbeats. Existing terminal cancellation remains
|
|
|
186
189
|
available. See the [cancellation guide](docs/cooperative-cancellation-design.md)
|
|
187
190
|
for immutable context, operation policies, shielded cleanup and recovery.
|
|
188
191
|
|
|
192
|
+
A suspended replay is not a workflow exit. Python may run `finally` blocks and
|
|
193
|
+
context-manager exits when disposing of that replay. The SDK discards commands
|
|
194
|
+
and `ctx.logger` messages produced only by disposal; queries observe the state
|
|
195
|
+
at the pending command. Actual completion, failure and cooperative cancellation
|
|
196
|
+
run cleanup through the normal history, and cold replay uses its recorded
|
|
197
|
+
results. Put external cleanup in an Activity rather than direct I/O in a
|
|
198
|
+
finalizer, and use `ctx.cancellation_shield()` when that Activity must finish
|
|
199
|
+
after cancellation. The disposal guarantees require SDK 2.12.2 or later.
|
|
200
|
+
|
|
189
201
|
Both `await activity.context().heartbeat()` and synchronous
|
|
190
202
|
`activity.context().heartbeat_sync()` stop at an explicit Server refusal.
|
|
191
203
|
They raise `ActivityLeaseAuthorityLost` with its original `.reason` for an
|
|
@@ -142,6 +142,15 @@ independently of application heartbeats. Existing terminal cancellation remains
|
|
|
142
142
|
available. See the [cancellation guide](docs/cooperative-cancellation-design.md)
|
|
143
143
|
for immutable context, operation policies, shielded cleanup and recovery.
|
|
144
144
|
|
|
145
|
+
A suspended replay is not a workflow exit. Python may run `finally` blocks and
|
|
146
|
+
context-manager exits when disposing of that replay. The SDK discards commands
|
|
147
|
+
and `ctx.logger` messages produced only by disposal; queries observe the state
|
|
148
|
+
at the pending command. Actual completion, failure and cooperative cancellation
|
|
149
|
+
run cleanup through the normal history, and cold replay uses its recorded
|
|
150
|
+
results. Put external cleanup in an Activity rather than direct I/O in a
|
|
151
|
+
finalizer, and use `ctx.cancellation_shield()` when that Activity must finish
|
|
152
|
+
after cancellation. The disposal guarantees require SDK 2.12.2 or later.
|
|
153
|
+
|
|
145
154
|
Both `await activity.context().heartbeat()` and synchronous
|
|
146
155
|
`activity.context().heartbeat_sync()` stop at an explicit Server refusal.
|
|
147
156
|
They raise `ActivityLeaseAuthorityLost` with its original `.reason` for an
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "2.12.
|
|
7
|
+
version = "2.12.2"
|
|
8
8
|
description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -29,6 +29,9 @@ classifiers = [
|
|
|
29
29
|
"Programming Language :: Python :: 3.10",
|
|
30
30
|
"Programming Language :: Python :: 3.11",
|
|
31
31
|
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Programming Language :: Python :: 3.14",
|
|
34
|
+
"Programming Language :: Python :: 3.15",
|
|
32
35
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
33
36
|
"Typing :: Typed",
|
|
34
37
|
]
|
|
@@ -76,8 +79,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
76
79
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
77
80
|
|
|
78
81
|
[tool.durable-workflow]
|
|
79
|
-
product-train = "2.12.
|
|
80
|
-
registry-version = "2.12.
|
|
82
|
+
product-train = "2.12.2"
|
|
83
|
+
registry-version = "2.12.2"
|
|
81
84
|
supported-server-versions = "2.8.0"
|
|
82
85
|
worker-protocol-version = "1.20"
|
|
83
86
|
control-plane-version = "2"
|
|
@@ -30,8 +30,8 @@ import uuid
|
|
|
30
30
|
import weakref
|
|
31
31
|
from collections.abc import Callable, Generator, Iterable, Mapping, Sequence
|
|
32
32
|
from contextvars import ContextVar
|
|
33
|
-
from copy import copy
|
|
34
|
-
from dataclasses import dataclass, field
|
|
33
|
+
from copy import copy, deepcopy
|
|
34
|
+
from dataclasses import dataclass, field, replace
|
|
35
35
|
from datetime import datetime, timezone
|
|
36
36
|
from typing import Any, TypeVar, cast
|
|
37
37
|
|
|
@@ -2682,6 +2682,66 @@ class WorkflowContext:
|
|
|
2682
2682
|
|
|
2683
2683
|
|
|
2684
2684
|
# ── Replay ───────────────────────────────────────────────────────────
|
|
2685
|
+
_ReplayCommandT = TypeVar("_ReplayCommandT", bound=Command)
|
|
2686
|
+
|
|
2687
|
+
|
|
2688
|
+
def _snapshot_command(command: _ReplayCommandT) -> _ReplayCommandT:
|
|
2689
|
+
"""Detach payload containers while preserving predicates and group metadata."""
|
|
2690
|
+
snapshot = copy(command)
|
|
2691
|
+
for name, value in vars(command).items():
|
|
2692
|
+
if isinstance(value, dict | list | tuple | set | bytearray):
|
|
2693
|
+
object.__setattr__(snapshot, name, deepcopy(value))
|
|
2694
|
+
return snapshot
|
|
2695
|
+
|
|
2696
|
+
|
|
2697
|
+
def _snapshot_local_call(call: PreparedLocalActivityCall) -> PreparedLocalActivityCall:
|
|
2698
|
+
return replace(call, command=_snapshot_command(call.command), cleanup=deepcopy(call.cleanup))
|
|
2699
|
+
|
|
2700
|
+
|
|
2701
|
+
class _ReplayLifetime:
|
|
2702
|
+
"""Own one suspended replay until its result or handler is consumed."""
|
|
2703
|
+
|
|
2704
|
+
def __init__(self, logger: _ReplayLogger) -> None:
|
|
2705
|
+
self.logger = logger
|
|
2706
|
+
self.generator: Any = None
|
|
2707
|
+
self.awaitable: Any = None
|
|
2708
|
+
|
|
2709
|
+
def close(self) -> None:
|
|
2710
|
+
self.logger._set_replaying(True)
|
|
2711
|
+
awaitable, self.awaitable = self.awaitable, None
|
|
2712
|
+
if inspect.iscoroutine(awaitable):
|
|
2713
|
+
awaitable.close()
|
|
2714
|
+
generator, self.generator = self.generator, None
|
|
2715
|
+
if generator is None:
|
|
2716
|
+
return
|
|
2717
|
+
|
|
2718
|
+
# Keep delegated generators alive while closing from the inside out.
|
|
2719
|
+
# A finally block may yield during GeneratorExit; inject exit again
|
|
2720
|
+
# instead of advancing or submitting that discarded durable command.
|
|
2721
|
+
pending = [generator]
|
|
2722
|
+
finished: list[Any] = []
|
|
2723
|
+
while pending:
|
|
2724
|
+
current = pending[-1]
|
|
2725
|
+
child = getattr(current, "gi_yieldfrom", None)
|
|
2726
|
+
if child is not None and callable(getattr(child, "close", None)) and not any(
|
|
2727
|
+
child is closed for closed in finished
|
|
2728
|
+
):
|
|
2729
|
+
pending.append(child)
|
|
2730
|
+
continue
|
|
2731
|
+
close = getattr(current, "close", None)
|
|
2732
|
+
if close is not None:
|
|
2733
|
+
# Discard errors are not a logical workflow failure. Actual
|
|
2734
|
+
# success/failure/cancellation still runs through the driver.
|
|
2735
|
+
with contextlib.suppress(BaseException):
|
|
2736
|
+
close()
|
|
2737
|
+
if not inspect.isgenerator(current) or current.gi_frame is None:
|
|
2738
|
+
finished.append(pending.pop())
|
|
2739
|
+
|
|
2740
|
+
def __del__(self) -> None:
|
|
2741
|
+
with contextlib.suppress(BaseException):
|
|
2742
|
+
self.close()
|
|
2743
|
+
|
|
2744
|
+
|
|
2685
2745
|
@dataclass
|
|
2686
2746
|
class ReplayOutcome:
|
|
2687
2747
|
commands: list[Command]
|
|
@@ -2693,6 +2753,19 @@ class ReplayOutcome:
|
|
|
2693
2753
|
cancellation_scope_opening: CancellationScopeOpening | None = None
|
|
2694
2754
|
cancellation_scope_delivery: CancellationScopeDeliveryIntent | None = None
|
|
2695
2755
|
|
|
2756
|
+
def __post_init__(self) -> None:
|
|
2757
|
+
# This ownership is not a dataclass/wire field.
|
|
2758
|
+
self._lifetime: _ReplayLifetime | None = None
|
|
2759
|
+
|
|
2760
|
+
def close(self) -> None:
|
|
2761
|
+
lifetime, self._lifetime = self._lifetime, None
|
|
2762
|
+
if lifetime is not None:
|
|
2763
|
+
lifetime.close()
|
|
2764
|
+
|
|
2765
|
+
def __del__(self) -> None:
|
|
2766
|
+
with contextlib.suppress(BaseException):
|
|
2767
|
+
self.close()
|
|
2768
|
+
|
|
2696
2769
|
|
|
2697
2770
|
@dataclass(frozen=True)
|
|
2698
2771
|
class _ServiceCallDelivery:
|
|
@@ -2815,6 +2888,25 @@ class _ReplayState:
|
|
|
2815
2888
|
instance: Any
|
|
2816
2889
|
|
|
2817
2890
|
|
|
2891
|
+
def _consume_replay_result(state: _ReplayState, result: Any) -> Any:
|
|
2892
|
+
"""Detach a handler value before discarded replay cleanup can mutate it."""
|
|
2893
|
+
if inspect.isawaitable(result):
|
|
2894
|
+
if state.outcome._lifetime is not None:
|
|
2895
|
+
state.outcome._lifetime.awaitable = result
|
|
2896
|
+
|
|
2897
|
+
async def consume() -> Any:
|
|
2898
|
+
try:
|
|
2899
|
+
return deepcopy(await result)
|
|
2900
|
+
finally:
|
|
2901
|
+
state.outcome.close()
|
|
2902
|
+
|
|
2903
|
+
return consume()
|
|
2904
|
+
try:
|
|
2905
|
+
return deepcopy(result)
|
|
2906
|
+
finally:
|
|
2907
|
+
state.outcome.close()
|
|
2908
|
+
|
|
2909
|
+
|
|
2818
2910
|
@dataclass
|
|
2819
2911
|
class _PendingReceiver:
|
|
2820
2912
|
history_index: int
|
|
@@ -3160,6 +3252,7 @@ def _prepare_query_state(
|
|
|
3160
3252
|
workflow_cls,
|
|
3161
3253
|
history_events,
|
|
3162
3254
|
start_input,
|
|
3255
|
+
reconstruction_only=True,
|
|
3163
3256
|
workflow_id=workflow_id,
|
|
3164
3257
|
run_id=run_id,
|
|
3165
3258
|
payload_codec=payload_codec,
|
|
@@ -3174,26 +3267,35 @@ def _prepare_query_state(
|
|
|
3174
3267
|
raise QueryFailed(f"workflow replay failed before query: {exc}") from exc
|
|
3175
3268
|
if state.outcome.commands and isinstance(state.outcome.commands[0], FailWorkflow):
|
|
3176
3269
|
failure = state.outcome.commands[0]
|
|
3270
|
+
state.outcome.close()
|
|
3177
3271
|
raise QueryFailed(f"workflow replay failed before query: {failure.message}") from None
|
|
3178
3272
|
|
|
3179
3273
|
query_registry: dict[str, str] = getattr(workflow_cls, "__workflow_queries__", {}) or {}
|
|
3180
3274
|
method_name = query_registry.get(query_name)
|
|
3181
3275
|
if method_name is None:
|
|
3276
|
+
state.outcome.close()
|
|
3182
3277
|
raise QueryFailed(f"unknown query {query_name!r}")
|
|
3183
3278
|
|
|
3184
3279
|
handler = getattr(state.instance, method_name, None)
|
|
3185
3280
|
if handler is None:
|
|
3281
|
+
state.outcome.close()
|
|
3186
3282
|
raise QueryFailed(f"query handler {query_name!r} is not available")
|
|
3187
3283
|
|
|
3188
3284
|
def invoke() -> Any:
|
|
3189
3285
|
try:
|
|
3190
|
-
|
|
3286
|
+
result = _invoke_workflow_handler(
|
|
3191
3287
|
handler, list(args) if args is not None else [], workflow_cls, method_name, allow_async=True,
|
|
3192
3288
|
)
|
|
3289
|
+
return _consume_replay_result(state, result)
|
|
3193
3290
|
except QueryFailed:
|
|
3291
|
+
state.outcome.close()
|
|
3194
3292
|
raise
|
|
3195
3293
|
except Exception as exc:
|
|
3294
|
+
state.outcome.close()
|
|
3196
3295
|
raise QueryFailed(str(exc) or f"query {query_name!r} failed") from exc
|
|
3296
|
+
except BaseException:
|
|
3297
|
+
state.outcome.close()
|
|
3298
|
+
raise
|
|
3197
3299
|
|
|
3198
3300
|
return invoke
|
|
3199
3301
|
|
|
@@ -3263,6 +3365,7 @@ def apply_update(
|
|
|
3263
3365
|
workflow_cls,
|
|
3264
3366
|
events,
|
|
3265
3367
|
start_input,
|
|
3368
|
+
reconstruction_only=True,
|
|
3266
3369
|
workflow_id=workflow_id,
|
|
3267
3370
|
run_id=run_id,
|
|
3268
3371
|
payload_codec=payload_codec,
|
|
@@ -3337,10 +3440,14 @@ def apply_update(
|
|
|
3337
3440
|
)
|
|
3338
3441
|
|
|
3339
3442
|
try:
|
|
3443
|
+
if state.outcome._lifetime is not None:
|
|
3444
|
+
state.outcome._lifetime.logger._set_replaying(False)
|
|
3340
3445
|
result = _invoke_workflow_handler(handler, args, workflow_cls, method_name)
|
|
3341
|
-
return CompleteUpdate(update_id=update_id, result=result)
|
|
3446
|
+
return CompleteUpdate(update_id=update_id, result=deepcopy(result))
|
|
3342
3447
|
except Exception as exc:
|
|
3343
3448
|
return _fail_update_from_exception(update_id, "update handler failed", exc)
|
|
3449
|
+
finally:
|
|
3450
|
+
state.outcome.close()
|
|
3344
3451
|
|
|
3345
3452
|
|
|
3346
3453
|
@dataclass(frozen=True)
|
|
@@ -3414,6 +3521,7 @@ def _prepare_update_validation(
|
|
|
3414
3521
|
workflow_cls,
|
|
3415
3522
|
events,
|
|
3416
3523
|
start_input,
|
|
3524
|
+
reconstruction_only=True,
|
|
3417
3525
|
workflow_id=replay_options.workflow_id,
|
|
3418
3526
|
run_id=replay_options.run_id,
|
|
3419
3527
|
payload_codec=replay_options.payload_codec,
|
|
@@ -3430,10 +3538,12 @@ def _prepare_update_validation(
|
|
|
3430
3538
|
|
|
3431
3539
|
if state.outcome.commands and isinstance(state.outcome.commands[0], FailWorkflow):
|
|
3432
3540
|
failure = state.outcome.commands[0]
|
|
3541
|
+
state.outcome.close()
|
|
3433
3542
|
raise UpdateValidationFailed(f"workflow replay failed before update validation: {failure.message}")
|
|
3434
3543
|
|
|
3435
3544
|
update_registry: dict[str, str] = getattr(workflow_cls, "__workflow_updates__", {}) or {}
|
|
3436
3545
|
if update_name not in update_registry:
|
|
3546
|
+
state.outcome.close()
|
|
3437
3547
|
raise UpdateValidationFailed(f"unknown update {update_name!r}")
|
|
3438
3548
|
|
|
3439
3549
|
validator_registry: dict[str, str] = (
|
|
@@ -3446,13 +3556,23 @@ def _prepare_update_validation(
|
|
|
3446
3556
|
)
|
|
3447
3557
|
validator_name = validator_registry.get(update_name)
|
|
3448
3558
|
if validator_name is None:
|
|
3559
|
+
state.outcome.close()
|
|
3449
3560
|
raise UpdateValidationFailed(f"update validator {update_name!r} is not declared")
|
|
3450
3561
|
|
|
3451
3562
|
validator = getattr(state.instance, validator_name, None)
|
|
3452
3563
|
if validator is None:
|
|
3564
|
+
state.outcome.close()
|
|
3453
3565
|
raise UpdateValidationFailed(f"update validator {update_name!r} is not available")
|
|
3454
3566
|
|
|
3455
|
-
|
|
3567
|
+
def invoke() -> Any:
|
|
3568
|
+
try:
|
|
3569
|
+
result = _invoke_update_validator(validator, args, workflow_cls, validator_name, update_name)
|
|
3570
|
+
return _consume_replay_result(state, result)
|
|
3571
|
+
except BaseException:
|
|
3572
|
+
state.outcome.close()
|
|
3573
|
+
raise
|
|
3574
|
+
|
|
3575
|
+
return invoke
|
|
3456
3576
|
|
|
3457
3577
|
|
|
3458
3578
|
def _invoke_update_validator(
|
|
@@ -4351,6 +4471,7 @@ def _replay_state(
|
|
|
4351
4471
|
prepare_local_activity_groups: bool = False,
|
|
4352
4472
|
local_activity_cancellation_policies: tuple[str, ...] = (),
|
|
4353
4473
|
stop_at_uncommitted_cancellation: bool = False,
|
|
4474
|
+
reconstruction_only: bool = False,
|
|
4354
4475
|
allow_cancellation_scope_authoring: bool = False,
|
|
4355
4476
|
allow_cancellation_scope_delivery: bool = False,
|
|
4356
4477
|
) -> _ReplayState:
|
|
@@ -4582,6 +4703,11 @@ def _replay_state(
|
|
|
4582
4703
|
_allow_wall_clock=not events,
|
|
4583
4704
|
)
|
|
4584
4705
|
ctx._replay_clock_error = replay_clock_error if workflow_start_time is None else None
|
|
4706
|
+
lifetime = _ReplayLifetime(ctx.logger)
|
|
4707
|
+
|
|
4708
|
+
def _set_replaying(replaying: bool) -> None:
|
|
4709
|
+
ctx.logger._set_replaying(replaying or reconstruction_only)
|
|
4710
|
+
|
|
4585
4711
|
# Continue-as-new cursor checkpoints describe instance state that predates
|
|
4586
4712
|
# this run. Seed them before the workflow opens its first stream wait so
|
|
4587
4713
|
# both the durable wait key and worker acknowledgement use the global
|
|
@@ -4614,25 +4740,32 @@ def _replay_state(
|
|
|
4614
4740
|
prepared_local_activity_group: PreparedLocalActivityGroup | None = None,
|
|
4615
4741
|
cancellation_scope_opening: CancellationScopeOpening | None = None,
|
|
4616
4742
|
) -> _ReplayState:
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4743
|
+
if prepared_local_activity is not None:
|
|
4744
|
+
prepared_local_activity = _snapshot_local_call(prepared_local_activity)
|
|
4745
|
+
if prepared_local_activity_group is not None:
|
|
4746
|
+
prepared_local_activity_group = replace(
|
|
4747
|
+
prepared_local_activity_group,
|
|
4748
|
+
commands=tuple(_snapshot_command(command) for command in prepared_local_activity_group.commands),
|
|
4749
|
+
calls=tuple(_snapshot_local_call(call) for call in prepared_local_activity_group.calls),
|
|
4750
|
+
)
|
|
4751
|
+
outcome = ReplayOutcome(
|
|
4752
|
+
commands=[_snapshot_command(command) for command in commands],
|
|
4753
|
+
cancellation_delivery=cancellation_intent,
|
|
4754
|
+
prepared_local_activity=prepared_local_activity,
|
|
4755
|
+
prepared_local_activity_group=prepared_local_activity_group,
|
|
4756
|
+
cancellation_scope_opening=cancellation_scope_opening,
|
|
4757
|
+
cancellation_scope_delivery=scope_intent,
|
|
4758
|
+
message_stream_cursors=[
|
|
4759
|
+
{"stream_name": name, "through_position": position}
|
|
4760
|
+
for name, position in sorted(ctx._message_stream_cursors.items())
|
|
4761
|
+
],
|
|
4762
|
+
message_stream_waits=[
|
|
4763
|
+
{"stream_name": name, "after_position": position}
|
|
4764
|
+
for name, position in sorted(ctx._message_stream_waits.items())
|
|
4765
|
+
],
|
|
4635
4766
|
)
|
|
4767
|
+
outcome._lifetime = lifetime
|
|
4768
|
+
return _ReplayState(outcome=outcome, instance=instance)
|
|
4636
4769
|
|
|
4637
4770
|
resolved_results: list[Any] = []
|
|
4638
4771
|
recorded_steps: list[_RecordedStep] = []
|
|
@@ -4661,6 +4794,7 @@ def _replay_state(
|
|
|
4661
4794
|
# Receiver replay uses this exact boundary so interleaved input is applied
|
|
4662
4795
|
# in durable order without leaking input recorded after the terminal row.
|
|
4663
4796
|
condition_wait_terminal_indexes: dict[str, int] = {}
|
|
4797
|
+
condition_wait_opened_indexes: dict[str, int] = {}
|
|
4664
4798
|
# Selection members are bound by their durable sequence instead of being
|
|
4665
4799
|
# inserted into the ordinary completion-order cursor. This is what lets a
|
|
4666
4800
|
# loser complete before or after unrelated successor work without changing
|
|
@@ -5006,6 +5140,7 @@ def _replay_state(
|
|
|
5006
5140
|
wait_id = payload.get("condition_wait_id")
|
|
5007
5141
|
if not isinstance(wait_id, str) or not wait_id:
|
|
5008
5142
|
continue
|
|
5143
|
+
condition_wait_opened_indexes.setdefault(wait_id, index)
|
|
5009
5144
|
|
|
5010
5145
|
if current_wait_id is None:
|
|
5011
5146
|
for receiver_index in prefix_receivers:
|
|
@@ -5070,7 +5205,14 @@ def _replay_state(
|
|
|
5070
5205
|
|
|
5071
5206
|
for receiver_index in receivers_since_wait:
|
|
5072
5207
|
if receiver_index is not None and receiver_index not in bindings:
|
|
5073
|
-
|
|
5208
|
+
if boundary_kind == "condition":
|
|
5209
|
+
# A terminal row can precede the next physical reopen.
|
|
5210
|
+
# Later inputs admitted during the first receiver's
|
|
5211
|
+
# leased replay belong to that reopen, not the wait
|
|
5212
|
+
# already resolved by the first receiver.
|
|
5213
|
+
prefix_receivers.append(receiver_index)
|
|
5214
|
+
else:
|
|
5215
|
+
bindings[receiver_index] = current_wait_id
|
|
5074
5216
|
current_wait_id = None
|
|
5075
5217
|
receivers_since_wait = []
|
|
5076
5218
|
prefix_can_bind_to_first_wait = boundary_kind == "condition"
|
|
@@ -5400,7 +5542,7 @@ def _replay_state(
|
|
|
5400
5542
|
if receiver.kind == "signal":
|
|
5401
5543
|
return
|
|
5402
5544
|
raise TypeError(f"update handler {receiver.name!r} is not available")
|
|
5403
|
-
|
|
5545
|
+
_set_replaying(True)
|
|
5404
5546
|
_invoke_workflow_handler(handler, receiver.args, workflow_cls, method_name)
|
|
5405
5547
|
|
|
5406
5548
|
def _receiver_due(receiver: _PendingReceiver, *, before_consuming_result: bool) -> bool:
|
|
@@ -5431,6 +5573,11 @@ def _replay_state(
|
|
|
5431
5573
|
terminal_index = condition_wait_terminal_indexes.get(condition_wait_id)
|
|
5432
5574
|
if terminal_index is not None:
|
|
5433
5575
|
while pending_receivers and pending_receivers[0].history_index < terminal_index:
|
|
5576
|
+
receiver_wait_id = pending_receivers[0].condition_wait_id
|
|
5577
|
+
receiver_wait_index = (condition_wait_opened_indexes.get(receiver_wait_id)
|
|
5578
|
+
if receiver_wait_id is not None else None)
|
|
5579
|
+
if receiver_wait_index is not None and receiver_wait_index > terminal_index:
|
|
5580
|
+
break
|
|
5434
5581
|
_apply_receiver(pending_receivers.pop(0))
|
|
5435
5582
|
return
|
|
5436
5583
|
target_index = next(
|
|
@@ -5503,6 +5650,25 @@ def _replay_state(
|
|
|
5503
5650
|
result_cursor = 0
|
|
5504
5651
|
pending_step_cursor = 0
|
|
5505
5652
|
wait_yield_count = 0
|
|
5653
|
+
has_terminal_history = any(_history_event_type(event) in {
|
|
5654
|
+
"WorkflowCompleted", "WorkflowFailed", "WorkflowCancelled", "WorkflowTerminated",
|
|
5655
|
+
"WorkflowContinuedAsNew", "WorkflowTimedOut",
|
|
5656
|
+
} for event in events)
|
|
5657
|
+
|
|
5658
|
+
def _refresh_replay_logging() -> None:
|
|
5659
|
+
# The resumed Python code is live before it yields its next command.
|
|
5660
|
+
# Read-only reconstruction and discarded-generator disposal stay quiet.
|
|
5661
|
+
_set_replaying(
|
|
5662
|
+
has_terminal_history
|
|
5663
|
+
or result_cursor < len(recorded_steps)
|
|
5664
|
+
or wait_yield_count < len(recorded_wait_steps)
|
|
5665
|
+
or pending_step_cursor < len(recorded_pending_steps)
|
|
5666
|
+
or selection_marker_cursor < len(selection_markers)
|
|
5667
|
+
or bool(pending_receivers)
|
|
5668
|
+
or (cancellation.delivery is not None and not cancellation_consumed)
|
|
5669
|
+
or len(consumed_scope_deliveries) < len(committed_scopes.deliveries)
|
|
5670
|
+
)
|
|
5671
|
+
|
|
5506
5672
|
entry = instance.run
|
|
5507
5673
|
workflow_type = str(getattr(workflow_cls, "__workflow_name__", workflow_cls.__name__))
|
|
5508
5674
|
try:
|
|
@@ -5517,6 +5683,7 @@ def _replay_state(
|
|
|
5517
5683
|
# Binding happens outside the application-error path. A TypeError raised
|
|
5518
5684
|
# by a successfully bound body is still an actual workflow failure.
|
|
5519
5685
|
try:
|
|
5686
|
+
_refresh_replay_logging()
|
|
5520
5687
|
gen = entry(ctx, *start_input)
|
|
5521
5688
|
except NonDeterministicReplayError:
|
|
5522
5689
|
raise
|
|
@@ -5534,7 +5701,9 @@ def _replay_state(
|
|
|
5534
5701
|
_assert_no_unconsumed_history("complete workflow")
|
|
5535
5702
|
return _state([CompleteWorkflow(result=gen)])
|
|
5536
5703
|
|
|
5537
|
-
|
|
5704
|
+
lifetime.generator = gen
|
|
5705
|
+
|
|
5706
|
+
_set_replaying(True)
|
|
5538
5707
|
|
|
5539
5708
|
next_value: Any = None
|
|
5540
5709
|
first = True
|
|
@@ -5617,7 +5786,7 @@ def _replay_state(
|
|
|
5617
5786
|
version = command.version
|
|
5618
5787
|
if type(version) is not int:
|
|
5619
5788
|
raise _version_error(f"Invalid version for change ID {change_id!r}.")
|
|
5620
|
-
|
|
5789
|
+
_set_replaying(False)
|
|
5621
5790
|
pending.append(command)
|
|
5622
5791
|
authored_sequence += 1
|
|
5623
5792
|
if not command.min_supported <= version <= command.max_supported:
|
|
@@ -6440,6 +6609,7 @@ def _replay_state(
|
|
|
6440
6609
|
if isinstance(command, DurableOperationHandle):
|
|
6441
6610
|
authored_sequence += 1
|
|
6442
6611
|
try:
|
|
6612
|
+
_refresh_replay_logging()
|
|
6443
6613
|
advanced_cmd = gen.throw(WorkflowCancelled(
|
|
6444
6614
|
"workflow cancellation was requested", request_id=boundary.request_id,
|
|
6445
6615
|
context=ctx._cancellation_context,
|
|
@@ -6496,6 +6666,7 @@ def _replay_state(
|
|
|
6496
6666
|
ctx._cancellation_context = active_context
|
|
6497
6667
|
_apply_due_receivers()
|
|
6498
6668
|
try:
|
|
6669
|
+
_refresh_replay_logging()
|
|
6499
6670
|
advanced_cmd = gen.throw(WorkflowCancelled(
|
|
6500
6671
|
"scope cancellation was requested", request_id=active_context.request_id, context=active_context,
|
|
6501
6672
|
))
|
|
@@ -6531,6 +6702,7 @@ def _replay_state(
|
|
|
6531
6702
|
advanced_cmd = None
|
|
6532
6703
|
else:
|
|
6533
6704
|
try:
|
|
6705
|
+
_refresh_replay_logging()
|
|
6534
6706
|
cmd = gen.send(None) if first else gen.send(next_value)
|
|
6535
6707
|
except StopIteration as stop:
|
|
6536
6708
|
return _terminal_state(stop.value, include_pending=True)
|
|
@@ -6574,7 +6746,7 @@ def _replay_state(
|
|
|
6574
6746
|
current_call_sequence, "cancellation scope", next_step.event_types if next_step else [],
|
|
6575
6747
|
detail="cancellation_scope_opening_changed: authored scope has no original canonical opening",
|
|
6576
6748
|
)
|
|
6577
|
-
|
|
6749
|
+
_set_replaying(False)
|
|
6578
6750
|
return _state(pending, cancellation_scope_opening=CancellationScopeOpening(
|
|
6579
6751
|
current_call_sequence, cmd.parent_scope_id, cmd.shield_parent,
|
|
6580
6752
|
))
|
|
@@ -6696,7 +6868,7 @@ def _replay_state(
|
|
|
6696
6868
|
if marker is None:
|
|
6697
6869
|
if has_recorded_member_history:
|
|
6698
6870
|
return _state(pending)
|
|
6699
|
-
|
|
6871
|
+
_set_replaying(False)
|
|
6700
6872
|
pending.extend(annotated)
|
|
6701
6873
|
return _state(pending)
|
|
6702
6874
|
|
|
@@ -6785,7 +6957,7 @@ def _replay_state(
|
|
|
6785
6957
|
if prepared_group:
|
|
6786
6958
|
group = _prepared_group(cmd)
|
|
6787
6959
|
if group is not None:
|
|
6788
|
-
|
|
6960
|
+
_set_replaying(False)
|
|
6789
6961
|
return _state(pending, prepared_local_activity_group=group)
|
|
6790
6962
|
if any(current_call_sequence + offset not in resolved_sequences for offset in range(needed)):
|
|
6791
6963
|
# Local results are durable. Remote members own their
|
|
@@ -6815,13 +6987,14 @@ def _replay_state(
|
|
|
6815
6987
|
_advance_cancellation_clock(consumed_indexes)
|
|
6816
6988
|
if failed is not None:
|
|
6817
6989
|
try:
|
|
6990
|
+
_refresh_replay_logging()
|
|
6818
6991
|
advanced_cmd = gen.throw(failed)
|
|
6819
6992
|
continue
|
|
6820
6993
|
except StopIteration as stop:
|
|
6821
6994
|
return _terminal_state(stop.value, include_pending=False)
|
|
6822
6995
|
next_value = _parallel_result_shape(result_shape, vals)
|
|
6823
6996
|
continue
|
|
6824
|
-
|
|
6997
|
+
_set_replaying(False)
|
|
6825
6998
|
for offset, child_command in enumerate(cmd):
|
|
6826
6999
|
_assert_pending_step_matches(child_command, offset)
|
|
6827
7000
|
pending.extend(cmd)
|
|
@@ -6833,6 +7006,7 @@ def _replay_state(
|
|
|
6833
7006
|
cancelled_selection_indexes[(cmd.selection_group_id, cmd.base_sequence)],
|
|
6834
7007
|
])
|
|
6835
7008
|
try:
|
|
7009
|
+
_refresh_replay_logging()
|
|
6836
7010
|
advanced_cmd = gen.throw(
|
|
6837
7011
|
DurableOperationCancelled(
|
|
6838
7012
|
cmd.selection_group_id,
|
|
@@ -6860,11 +7034,12 @@ def _replay_state(
|
|
|
6860
7034
|
selection_shape,
|
|
6861
7035
|
)
|
|
6862
7036
|
if not resolved:
|
|
6863
|
-
|
|
7037
|
+
_set_replaying(False)
|
|
6864
7038
|
return _state(pending)
|
|
6865
7039
|
_advance_selection_clock(cmd.base_sequence, cmd.size, failure)
|
|
6866
7040
|
if failure is not None:
|
|
6867
7041
|
try:
|
|
7042
|
+
_refresh_replay_logging()
|
|
6868
7043
|
advanced_cmd = gen.throw(failure)
|
|
6869
7044
|
continue
|
|
6870
7045
|
except StopIteration as stop:
|
|
@@ -6929,12 +7104,13 @@ def _replay_state(
|
|
|
6929
7104
|
)
|
|
6930
7105
|
except NexusOperationFailed as exc:
|
|
6931
7106
|
try:
|
|
7107
|
+
_refresh_replay_logging()
|
|
6932
7108
|
advanced_cmd = gen.throw(exc)
|
|
6933
7109
|
continue
|
|
6934
7110
|
except StopIteration as stop:
|
|
6935
7111
|
return _terminal_state(stop.value, include_pending=False)
|
|
6936
7112
|
continue
|
|
6937
|
-
|
|
7113
|
+
_set_replaying(False)
|
|
6938
7114
|
_assert_pending_step_matches(cmd)
|
|
6939
7115
|
if _next_unconsumed_recorded_step() is not None:
|
|
6940
7116
|
# Admission already has its durable sequence. A later task
|
|
@@ -6949,13 +7125,19 @@ def _replay_state(
|
|
|
6949
7125
|
next_value = resolved_results[result_cursor]
|
|
6950
7126
|
result_cursor += 1
|
|
6951
7127
|
continue
|
|
6952
|
-
|
|
7128
|
+
_set_replaying(False)
|
|
6953
7129
|
_assert_pending_step_matches(cmd)
|
|
7130
|
+
if reconstruction_only:
|
|
7131
|
+
# A query/update rebuild cannot author a missing marker or
|
|
7132
|
+
# execute its callback. Only a workflow task may do that.
|
|
7133
|
+
pending.append(cmd)
|
|
7134
|
+
return _state(pending)
|
|
6954
7135
|
if cmd._effect is not None:
|
|
6955
7136
|
try:
|
|
6956
7137
|
cmd.result = cmd._effect()
|
|
6957
7138
|
except Exception as exc:
|
|
6958
7139
|
try:
|
|
7140
|
+
_refresh_replay_logging()
|
|
6959
7141
|
advanced_cmd = gen.throw(exc)
|
|
6960
7142
|
continue
|
|
6961
7143
|
except StopIteration as stop:
|
|
@@ -6970,7 +7152,7 @@ def _replay_state(
|
|
|
6970
7152
|
next_value = resolved_results[result_cursor]
|
|
6971
7153
|
result_cursor += 1
|
|
6972
7154
|
continue
|
|
6973
|
-
|
|
7155
|
+
_set_replaying(False)
|
|
6974
7156
|
_assert_pending_step_matches(cmd)
|
|
6975
7157
|
pending.append(cmd)
|
|
6976
7158
|
next_value = None
|
|
@@ -7012,7 +7194,7 @@ def _replay_state(
|
|
|
7012
7194
|
step = _next_unconsumed_recorded_step()
|
|
7013
7195
|
if step is not None:
|
|
7014
7196
|
_assert_step_matches(cmd, step)
|
|
7015
|
-
|
|
7197
|
+
_set_replaying(False)
|
|
7016
7198
|
pending.append(cmd)
|
|
7017
7199
|
return _state(pending)
|
|
7018
7200
|
# Terminal cleanup may only skip a later open after this
|
|
@@ -7119,7 +7301,7 @@ def _replay_state(
|
|
|
7119
7301
|
wait_yield_count = next_wait_index
|
|
7120
7302
|
continue
|
|
7121
7303
|
|
|
7122
|
-
|
|
7304
|
+
_set_replaying(False)
|
|
7123
7305
|
pending.append(cmd)
|
|
7124
7306
|
wait_yield_count = next_wait_index
|
|
7125
7307
|
return _state(pending)
|
|
@@ -7133,13 +7315,14 @@ def _replay_state(
|
|
|
7133
7315
|
result_cursor += 1
|
|
7134
7316
|
if isinstance(val, ActivityFailed | ChildWorkflowFailed):
|
|
7135
7317
|
try:
|
|
7318
|
+
_refresh_replay_logging()
|
|
7136
7319
|
advanced_cmd = gen.throw(val)
|
|
7137
7320
|
continue
|
|
7138
7321
|
except StopIteration as stop:
|
|
7139
7322
|
return _terminal_state(stop.value, include_pending=False)
|
|
7140
7323
|
next_value = val
|
|
7141
7324
|
continue
|
|
7142
|
-
|
|
7325
|
+
_set_replaying(False)
|
|
7143
7326
|
_assert_pending_step_matches(cmd)
|
|
7144
7327
|
if not isinstance(cmd, RecordLocalActivity) and _next_unconsumed_recorded_step() is not None:
|
|
7145
7328
|
# A signal may wake a workflow while its existing operation is
|
|
@@ -7155,6 +7338,7 @@ def _replay_state(
|
|
|
7155
7338
|
pending.append(cmd)
|
|
7156
7339
|
if isinstance(local_result, ActivityFailed):
|
|
7157
7340
|
try:
|
|
7341
|
+
_refresh_replay_logging()
|
|
7158
7342
|
advanced_cmd = gen.throw(local_result)
|
|
7159
7343
|
continue
|
|
7160
7344
|
except StopIteration as stop:
|