durable-workflow 2.12.1__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.1/src/durable_workflow.egg-info → durable_workflow-2.12.2}/PKG-INFO +13 -1
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/README.md +9 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/pyproject.toml +6 -3
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/workflow.py +207 -37
- {durable_workflow-2.12.1 → durable_workflow-2.12.2/src/durable_workflow.egg-info}/PKG-INFO +13 -1
- {durable_workflow-2.12.1 → 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.1 → durable_workflow-2.12.2}/tests/test_replay_regression_corpus.py +24 -7
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/LICENSE +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/setup.cfg +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_activity_lease.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_failure_reporting.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_metric_routes.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_poll_transport.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/telemetry.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/telemetry_carrier.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/worker.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_heartbeat_authority.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_lease_authority.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_lease_renewal.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_lease_worker.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_retry_info.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_client.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_client_telemetry.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_errors.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_failure_reporting.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_history_bundle_schema2.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_invocable.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_local_failure_reporting.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_metric_routes.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_metrics.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_poll_failure_backoff.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_poll_transport.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_queries.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_replay.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_replay_clock.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_schedules.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_sdk_metrics_contract.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_serializer.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_signals.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_sleep.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_sync.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_sync_activities.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_telemetry.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_telemetry_carrier.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_testing_failures.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_testing_nexus.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_threaded_heartbeat_example.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_tls_trust.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_transport_diagnostics.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_updates.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_worker.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_worker_deregistration.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_worker_observation.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_worker_telemetry_wire.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_entry_arguments.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_handler_validation.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_identity.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.12.1 → 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] = []
|
|
@@ -5409,7 +5542,7 @@ def _replay_state(
|
|
|
5409
5542
|
if receiver.kind == "signal":
|
|
5410
5543
|
return
|
|
5411
5544
|
raise TypeError(f"update handler {receiver.name!r} is not available")
|
|
5412
|
-
|
|
5545
|
+
_set_replaying(True)
|
|
5413
5546
|
_invoke_workflow_handler(handler, receiver.args, workflow_cls, method_name)
|
|
5414
5547
|
|
|
5415
5548
|
def _receiver_due(receiver: _PendingReceiver, *, before_consuming_result: bool) -> bool:
|
|
@@ -5517,6 +5650,25 @@ def _replay_state(
|
|
|
5517
5650
|
result_cursor = 0
|
|
5518
5651
|
pending_step_cursor = 0
|
|
5519
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
|
+
|
|
5520
5672
|
entry = instance.run
|
|
5521
5673
|
workflow_type = str(getattr(workflow_cls, "__workflow_name__", workflow_cls.__name__))
|
|
5522
5674
|
try:
|
|
@@ -5531,6 +5683,7 @@ def _replay_state(
|
|
|
5531
5683
|
# Binding happens outside the application-error path. A TypeError raised
|
|
5532
5684
|
# by a successfully bound body is still an actual workflow failure.
|
|
5533
5685
|
try:
|
|
5686
|
+
_refresh_replay_logging()
|
|
5534
5687
|
gen = entry(ctx, *start_input)
|
|
5535
5688
|
except NonDeterministicReplayError:
|
|
5536
5689
|
raise
|
|
@@ -5548,7 +5701,9 @@ def _replay_state(
|
|
|
5548
5701
|
_assert_no_unconsumed_history("complete workflow")
|
|
5549
5702
|
return _state([CompleteWorkflow(result=gen)])
|
|
5550
5703
|
|
|
5551
|
-
|
|
5704
|
+
lifetime.generator = gen
|
|
5705
|
+
|
|
5706
|
+
_set_replaying(True)
|
|
5552
5707
|
|
|
5553
5708
|
next_value: Any = None
|
|
5554
5709
|
first = True
|
|
@@ -5631,7 +5786,7 @@ def _replay_state(
|
|
|
5631
5786
|
version = command.version
|
|
5632
5787
|
if type(version) is not int:
|
|
5633
5788
|
raise _version_error(f"Invalid version for change ID {change_id!r}.")
|
|
5634
|
-
|
|
5789
|
+
_set_replaying(False)
|
|
5635
5790
|
pending.append(command)
|
|
5636
5791
|
authored_sequence += 1
|
|
5637
5792
|
if not command.min_supported <= version <= command.max_supported:
|
|
@@ -6454,6 +6609,7 @@ def _replay_state(
|
|
|
6454
6609
|
if isinstance(command, DurableOperationHandle):
|
|
6455
6610
|
authored_sequence += 1
|
|
6456
6611
|
try:
|
|
6612
|
+
_refresh_replay_logging()
|
|
6457
6613
|
advanced_cmd = gen.throw(WorkflowCancelled(
|
|
6458
6614
|
"workflow cancellation was requested", request_id=boundary.request_id,
|
|
6459
6615
|
context=ctx._cancellation_context,
|
|
@@ -6510,6 +6666,7 @@ def _replay_state(
|
|
|
6510
6666
|
ctx._cancellation_context = active_context
|
|
6511
6667
|
_apply_due_receivers()
|
|
6512
6668
|
try:
|
|
6669
|
+
_refresh_replay_logging()
|
|
6513
6670
|
advanced_cmd = gen.throw(WorkflowCancelled(
|
|
6514
6671
|
"scope cancellation was requested", request_id=active_context.request_id, context=active_context,
|
|
6515
6672
|
))
|
|
@@ -6545,6 +6702,7 @@ def _replay_state(
|
|
|
6545
6702
|
advanced_cmd = None
|
|
6546
6703
|
else:
|
|
6547
6704
|
try:
|
|
6705
|
+
_refresh_replay_logging()
|
|
6548
6706
|
cmd = gen.send(None) if first else gen.send(next_value)
|
|
6549
6707
|
except StopIteration as stop:
|
|
6550
6708
|
return _terminal_state(stop.value, include_pending=True)
|
|
@@ -6588,7 +6746,7 @@ def _replay_state(
|
|
|
6588
6746
|
current_call_sequence, "cancellation scope", next_step.event_types if next_step else [],
|
|
6589
6747
|
detail="cancellation_scope_opening_changed: authored scope has no original canonical opening",
|
|
6590
6748
|
)
|
|
6591
|
-
|
|
6749
|
+
_set_replaying(False)
|
|
6592
6750
|
return _state(pending, cancellation_scope_opening=CancellationScopeOpening(
|
|
6593
6751
|
current_call_sequence, cmd.parent_scope_id, cmd.shield_parent,
|
|
6594
6752
|
))
|
|
@@ -6710,7 +6868,7 @@ def _replay_state(
|
|
|
6710
6868
|
if marker is None:
|
|
6711
6869
|
if has_recorded_member_history:
|
|
6712
6870
|
return _state(pending)
|
|
6713
|
-
|
|
6871
|
+
_set_replaying(False)
|
|
6714
6872
|
pending.extend(annotated)
|
|
6715
6873
|
return _state(pending)
|
|
6716
6874
|
|
|
@@ -6799,7 +6957,7 @@ def _replay_state(
|
|
|
6799
6957
|
if prepared_group:
|
|
6800
6958
|
group = _prepared_group(cmd)
|
|
6801
6959
|
if group is not None:
|
|
6802
|
-
|
|
6960
|
+
_set_replaying(False)
|
|
6803
6961
|
return _state(pending, prepared_local_activity_group=group)
|
|
6804
6962
|
if any(current_call_sequence + offset not in resolved_sequences for offset in range(needed)):
|
|
6805
6963
|
# Local results are durable. Remote members own their
|
|
@@ -6829,13 +6987,14 @@ def _replay_state(
|
|
|
6829
6987
|
_advance_cancellation_clock(consumed_indexes)
|
|
6830
6988
|
if failed is not None:
|
|
6831
6989
|
try:
|
|
6990
|
+
_refresh_replay_logging()
|
|
6832
6991
|
advanced_cmd = gen.throw(failed)
|
|
6833
6992
|
continue
|
|
6834
6993
|
except StopIteration as stop:
|
|
6835
6994
|
return _terminal_state(stop.value, include_pending=False)
|
|
6836
6995
|
next_value = _parallel_result_shape(result_shape, vals)
|
|
6837
6996
|
continue
|
|
6838
|
-
|
|
6997
|
+
_set_replaying(False)
|
|
6839
6998
|
for offset, child_command in enumerate(cmd):
|
|
6840
6999
|
_assert_pending_step_matches(child_command, offset)
|
|
6841
7000
|
pending.extend(cmd)
|
|
@@ -6847,6 +7006,7 @@ def _replay_state(
|
|
|
6847
7006
|
cancelled_selection_indexes[(cmd.selection_group_id, cmd.base_sequence)],
|
|
6848
7007
|
])
|
|
6849
7008
|
try:
|
|
7009
|
+
_refresh_replay_logging()
|
|
6850
7010
|
advanced_cmd = gen.throw(
|
|
6851
7011
|
DurableOperationCancelled(
|
|
6852
7012
|
cmd.selection_group_id,
|
|
@@ -6874,11 +7034,12 @@ def _replay_state(
|
|
|
6874
7034
|
selection_shape,
|
|
6875
7035
|
)
|
|
6876
7036
|
if not resolved:
|
|
6877
|
-
|
|
7037
|
+
_set_replaying(False)
|
|
6878
7038
|
return _state(pending)
|
|
6879
7039
|
_advance_selection_clock(cmd.base_sequence, cmd.size, failure)
|
|
6880
7040
|
if failure is not None:
|
|
6881
7041
|
try:
|
|
7042
|
+
_refresh_replay_logging()
|
|
6882
7043
|
advanced_cmd = gen.throw(failure)
|
|
6883
7044
|
continue
|
|
6884
7045
|
except StopIteration as stop:
|
|
@@ -6943,12 +7104,13 @@ def _replay_state(
|
|
|
6943
7104
|
)
|
|
6944
7105
|
except NexusOperationFailed as exc:
|
|
6945
7106
|
try:
|
|
7107
|
+
_refresh_replay_logging()
|
|
6946
7108
|
advanced_cmd = gen.throw(exc)
|
|
6947
7109
|
continue
|
|
6948
7110
|
except StopIteration as stop:
|
|
6949
7111
|
return _terminal_state(stop.value, include_pending=False)
|
|
6950
7112
|
continue
|
|
6951
|
-
|
|
7113
|
+
_set_replaying(False)
|
|
6952
7114
|
_assert_pending_step_matches(cmd)
|
|
6953
7115
|
if _next_unconsumed_recorded_step() is not None:
|
|
6954
7116
|
# Admission already has its durable sequence. A later task
|
|
@@ -6963,13 +7125,19 @@ def _replay_state(
|
|
|
6963
7125
|
next_value = resolved_results[result_cursor]
|
|
6964
7126
|
result_cursor += 1
|
|
6965
7127
|
continue
|
|
6966
|
-
|
|
7128
|
+
_set_replaying(False)
|
|
6967
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)
|
|
6968
7135
|
if cmd._effect is not None:
|
|
6969
7136
|
try:
|
|
6970
7137
|
cmd.result = cmd._effect()
|
|
6971
7138
|
except Exception as exc:
|
|
6972
7139
|
try:
|
|
7140
|
+
_refresh_replay_logging()
|
|
6973
7141
|
advanced_cmd = gen.throw(exc)
|
|
6974
7142
|
continue
|
|
6975
7143
|
except StopIteration as stop:
|
|
@@ -6984,7 +7152,7 @@ def _replay_state(
|
|
|
6984
7152
|
next_value = resolved_results[result_cursor]
|
|
6985
7153
|
result_cursor += 1
|
|
6986
7154
|
continue
|
|
6987
|
-
|
|
7155
|
+
_set_replaying(False)
|
|
6988
7156
|
_assert_pending_step_matches(cmd)
|
|
6989
7157
|
pending.append(cmd)
|
|
6990
7158
|
next_value = None
|
|
@@ -7026,7 +7194,7 @@ def _replay_state(
|
|
|
7026
7194
|
step = _next_unconsumed_recorded_step()
|
|
7027
7195
|
if step is not None:
|
|
7028
7196
|
_assert_step_matches(cmd, step)
|
|
7029
|
-
|
|
7197
|
+
_set_replaying(False)
|
|
7030
7198
|
pending.append(cmd)
|
|
7031
7199
|
return _state(pending)
|
|
7032
7200
|
# Terminal cleanup may only skip a later open after this
|
|
@@ -7133,7 +7301,7 @@ def _replay_state(
|
|
|
7133
7301
|
wait_yield_count = next_wait_index
|
|
7134
7302
|
continue
|
|
7135
7303
|
|
|
7136
|
-
|
|
7304
|
+
_set_replaying(False)
|
|
7137
7305
|
pending.append(cmd)
|
|
7138
7306
|
wait_yield_count = next_wait_index
|
|
7139
7307
|
return _state(pending)
|
|
@@ -7147,13 +7315,14 @@ def _replay_state(
|
|
|
7147
7315
|
result_cursor += 1
|
|
7148
7316
|
if isinstance(val, ActivityFailed | ChildWorkflowFailed):
|
|
7149
7317
|
try:
|
|
7318
|
+
_refresh_replay_logging()
|
|
7150
7319
|
advanced_cmd = gen.throw(val)
|
|
7151
7320
|
continue
|
|
7152
7321
|
except StopIteration as stop:
|
|
7153
7322
|
return _terminal_state(stop.value, include_pending=False)
|
|
7154
7323
|
next_value = val
|
|
7155
7324
|
continue
|
|
7156
|
-
|
|
7325
|
+
_set_replaying(False)
|
|
7157
7326
|
_assert_pending_step_matches(cmd)
|
|
7158
7327
|
if not isinstance(cmd, RecordLocalActivity) and _next_unconsumed_recorded_step() is not None:
|
|
7159
7328
|
# A signal may wake a workflow while its existing operation is
|
|
@@ -7169,6 +7338,7 @@ def _replay_state(
|
|
|
7169
7338
|
pending.append(cmd)
|
|
7170
7339
|
if isinstance(local_result, ActivityFailed):
|
|
7171
7340
|
try:
|
|
7341
|
+
_refresh_replay_logging()
|
|
7172
7342
|
advanced_cmd = gen.throw(local_result)
|
|
7173
7343
|
continue
|
|
7174
7344
|
except StopIteration as stop:
|
|
@@ -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
|
{durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/SOURCES.txt
RENAMED
|
@@ -88,6 +88,7 @@ tests/test_external_storage.py
|
|
|
88
88
|
tests/test_external_task_input.py
|
|
89
89
|
tests/test_external_task_result.py
|
|
90
90
|
tests/test_failure_reporting.py
|
|
91
|
+
tests/test_generator_lifecycle.py
|
|
91
92
|
tests/test_golden_history_replay.py
|
|
92
93
|
tests/test_history_bundle_schema2.py
|
|
93
94
|
tests/test_history_bundle_verify.py
|