durable-workflow 2.14.0__tar.gz → 2.14.1__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.14.0/src/durable_workflow.egg-info → durable_workflow-2.14.1}/PKG-INFO +1 -1
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/pyproject.toml +3 -3
- durable_workflow-2.14.1/src/durable_workflow/_condition_fingerprint.py +229 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/workflow.py +80 -40
- {durable_workflow-2.14.0 → durable_workflow-2.14.1/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow.egg-info/SOURCES.txt +2 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_client_timeouts.py +5 -0
- durable_workflow-2.14.1/tests/test_condition_fingerprint.py +282 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_replay_regression_corpus.py +4 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_wait_condition.py +2 -1
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_worker.py +14 -5
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/LICENSE +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/README.md +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/setup.cfg +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_activity_lease.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_failure_reporting.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_metric_routes.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_poll_transport.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/_typed_values.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/server_compatibility.json +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/telemetry.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/telemetry_carrier.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/worker.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/worker_status.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_activity_heartbeat_authority.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_activity_lease_authority.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_activity_lease_renewal.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_activity_lease_worker.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_activity_retry_info.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_client.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_client_telemetry.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_empty_callback_failure.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_errors.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_failure_reporting.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_generator_lifecycle.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_history_bundle_pruned.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_history_bundle_schema2.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_invocable.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_local_failure_reporting.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_metric_routes.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_metrics.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_poll_failure_backoff.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_poll_transport.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_publish_workflow_isolation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_queries.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_redrive_successor_evidence.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_replay_clock.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_schedules.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_sdk_metrics_contract.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_search_attribute_replay_identity.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_serializer.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_signals.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_sleep.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_sync.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_sync_activities.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_telemetry.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_telemetry_carrier.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_testing_failures.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_testing_nexus.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_threaded_heartbeat_example.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_tls_trust.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_transport_diagnostics.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_typed_values.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_updates.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_worker_deregistration.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_worker_observation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_worker_status.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_worker_telemetry_wire.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_workflow_entry_arguments.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_workflow_handler_validation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_workflow_identity.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.14.1}/tests/test_workflow_updates_conformance.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "2.14.
|
|
7
|
+
version = "2.14.1"
|
|
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"
|
|
@@ -79,8 +79,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
79
79
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
80
80
|
|
|
81
81
|
[tool.durable-workflow]
|
|
82
|
-
product-train = "2.14.
|
|
83
|
-
registry-version = "2.14.
|
|
82
|
+
product-train = "2.14.1"
|
|
83
|
+
registry-version = "2.14.1"
|
|
84
84
|
supported-server-versions = "2.8.0"
|
|
85
85
|
supported-server-release-baselines = { "2" = "2.8.0", "3" = "3.0.0" }
|
|
86
86
|
worker-protocol-version = "1.20"
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Stable identity for ``wait_condition`` predicates (#258).
|
|
2
|
+
|
|
3
|
+
The Server records the fingerprint a worker sends with ``open_condition_wait``
|
|
4
|
+
and replay compares it, so the value must be the same on every Python version
|
|
5
|
+
and in every process. Version 2 hashes the predicate's module, qualified name
|
|
6
|
+
and a canonical form of its source AST. It never hashes bytecode, constant
|
|
7
|
+
reprs, memory addresses, file paths or line numbers.
|
|
8
|
+
|
|
9
|
+
Version 1 (every release through 2.14.0) hashed bytecode and ``repr`` of code
|
|
10
|
+
constants, which change across Python versions and, for nested functions,
|
|
11
|
+
across processes. Recorded v1 values cannot be recomputed portably, so replay
|
|
12
|
+
treats a v1 value that differs from this process's v1 as unverifiable rather
|
|
13
|
+
than as a changed predicate (see :func:`fingerprint_verdict`).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
import hashlib
|
|
20
|
+
import inspect
|
|
21
|
+
import linecache
|
|
22
|
+
import re
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from typing import Any, Literal
|
|
25
|
+
|
|
26
|
+
V2_PREFIX = "dwpy2:sha256:"
|
|
27
|
+
_LEGACY = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
28
|
+
# Fields that carry no predicate meaning or differ between Python versions.
|
|
29
|
+
_SKIPPED_FIELDS = frozenset({"ctx", "kind", "type_comment", "type_params", "decorator_list", "returns"})
|
|
30
|
+
_parsed: dict[tuple[str, int], ast.Module | None] = {}
|
|
31
|
+
_nodes: dict[Any, str | None] = {}
|
|
32
|
+
# Python 3.12 inlines list, dict and set comprehensions (PEP 709), which drops
|
|
33
|
+
# their segment from a nested function's __qualname__.
|
|
34
|
+
_INLINED_COMPREHENSION = re.compile(r"\.<(?:listcomp|dictcomp|setcomp)>")
|
|
35
|
+
|
|
36
|
+
Verdict = Literal["match", "mismatch", "unverifiable"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def legacy_fingerprint(predicate: Callable[[], bool]) -> str:
|
|
40
|
+
"""The v1 fingerprint, kept only to recognise histories recorded by 2.14.0 and earlier."""
|
|
41
|
+
h = hashlib.sha256()
|
|
42
|
+
h.update(b"durable-workflow-python.wait-condition.v1\0")
|
|
43
|
+
h.update(f"{getattr(predicate, '__module__', '')}\0".encode())
|
|
44
|
+
h.update(f"{getattr(predicate, '__qualname__', '')}\0".encode())
|
|
45
|
+
original = getattr(predicate, "__typed_workflow_method__", None)
|
|
46
|
+
code = getattr(original if original is not None else predicate, "__code__", None)
|
|
47
|
+
if code is None:
|
|
48
|
+
h.update(repr(predicate).encode())
|
|
49
|
+
else:
|
|
50
|
+
h.update(repr((
|
|
51
|
+
code.co_argcount, code.co_posonlyargcount, code.co_kwonlyargcount, code.co_code,
|
|
52
|
+
code.co_consts, code.co_names, code.co_varnames, code.co_freevars,
|
|
53
|
+
)).encode())
|
|
54
|
+
return f"sha256:{h.hexdigest()}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_legacy(value: Any) -> bool:
|
|
58
|
+
return isinstance(value, str) and _LEGACY.match(value) is not None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def fingerprint(predicate: Callable[[], bool]) -> str | None:
|
|
62
|
+
"""Return the v2 fingerprint, or None when the predicate's source is unavailable.
|
|
63
|
+
|
|
64
|
+
Without a fingerprint the wait is identified by its key and call order,
|
|
65
|
+
as in sdk-php.
|
|
66
|
+
"""
|
|
67
|
+
function = _authored_function(predicate)
|
|
68
|
+
if function is None:
|
|
69
|
+
return None
|
|
70
|
+
code = function.__code__
|
|
71
|
+
if code not in _nodes:
|
|
72
|
+
_nodes[code] = _predicate_node(function)
|
|
73
|
+
node = _nodes[code]
|
|
74
|
+
if node is None:
|
|
75
|
+
return None
|
|
76
|
+
qualname = _INLINED_COMPREHENSION.sub("", getattr(function, "__qualname__", ""))
|
|
77
|
+
h = hashlib.sha256()
|
|
78
|
+
h.update(b"durable-workflow-python.wait-condition.v2\0")
|
|
79
|
+
h.update(f"{getattr(function, '__module__', '')}\0".encode())
|
|
80
|
+
h.update(f"{qualname}\0".encode())
|
|
81
|
+
h.update(node.encode())
|
|
82
|
+
return f"{V2_PREFIX}{h.hexdigest()}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def fingerprint_verdict(recorded: Any, authored: str | None, predicate: Callable[[], bool]) -> Verdict:
|
|
86
|
+
"""Compare a recorded fingerprint with the authored predicate.
|
|
87
|
+
|
|
88
|
+
- No recorded value: nothing to compare.
|
|
89
|
+
- An identity supplied explicitly in another format (not v2, not None)
|
|
90
|
+
is compared exactly, as before.
|
|
91
|
+
- A v2 value is compared exactly; without source it cannot be checked.
|
|
92
|
+
- A v1 value equal to this process's v1 matches. Any other v1 value is
|
|
93
|
+
unverifiable, because v1 differs across Python versions and processes.
|
|
94
|
+
- Any other string is compared exactly, as before.
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(recorded, str):
|
|
97
|
+
return "match"
|
|
98
|
+
if authored is not None and not authored.startswith(V2_PREFIX):
|
|
99
|
+
return "match" if recorded == authored else "mismatch"
|
|
100
|
+
if recorded.startswith(V2_PREFIX):
|
|
101
|
+
if authored is None:
|
|
102
|
+
return "unverifiable"
|
|
103
|
+
return "match" if recorded == authored else "mismatch"
|
|
104
|
+
if is_legacy(recorded):
|
|
105
|
+
return "match" if recorded == legacy_fingerprint(predicate) else "unverifiable"
|
|
106
|
+
return "match" if recorded == authored else "mismatch"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _authored_function(predicate: Any) -> Any:
|
|
110
|
+
target = getattr(predicate, "__typed_workflow_method__", None) or predicate
|
|
111
|
+
if inspect.ismethod(target):
|
|
112
|
+
target = target.__func__
|
|
113
|
+
target = getattr(target, "__typed_workflow_method__", None) or target
|
|
114
|
+
return target if inspect.isfunction(target) else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _module_tree(function: Any) -> ast.Module | None:
|
|
118
|
+
try:
|
|
119
|
+
filename = inspect.getsourcefile(function) or inspect.getfile(function)
|
|
120
|
+
except (OSError, TypeError):
|
|
121
|
+
return None
|
|
122
|
+
lines = linecache.getlines(filename, function.__globals__)
|
|
123
|
+
if not lines:
|
|
124
|
+
return None
|
|
125
|
+
source = "".join(lines)
|
|
126
|
+
key = (filename, hash(source))
|
|
127
|
+
if key not in _parsed:
|
|
128
|
+
try:
|
|
129
|
+
_parsed[key] = ast.parse(source)
|
|
130
|
+
except (SyntaxError, ValueError):
|
|
131
|
+
_parsed[key] = None
|
|
132
|
+
return _parsed[key]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _predicate_node(function: Any) -> str | None:
|
|
136
|
+
tree = _module_tree(function)
|
|
137
|
+
if tree is None:
|
|
138
|
+
return None
|
|
139
|
+
code = function.__code__
|
|
140
|
+
first_line = code.co_firstlineno
|
|
141
|
+
if function.__name__ == "<lambda>":
|
|
142
|
+
lambdas = [node for node in ast.walk(tree) if isinstance(node, ast.Lambda) and node.lineno == first_line]
|
|
143
|
+
lambdas.sort(key=lambda node: node.col_offset)
|
|
144
|
+
# PEP 709 moves comprehension instructions into the enclosing lambda.
|
|
145
|
+
# Candidates indistinguishable before 3.12 can become distinct after
|
|
146
|
+
# inlining, so keep the combined AST on every version in this case.
|
|
147
|
+
has_inlined_comprehension = any(
|
|
148
|
+
isinstance(child, ast.ListComp | ast.DictComp | ast.SetComp)
|
|
149
|
+
for node in lambdas for child in ast.walk(node)
|
|
150
|
+
)
|
|
151
|
+
if len(lambdas) > 1 and not has_inlined_comprehension:
|
|
152
|
+
# Names and constants are stable across versions; bytecode only
|
|
153
|
+
# breaks a tie, and is compared within this interpreter.
|
|
154
|
+
matching = [node for node in lambdas if _compiles_to(node, code, compare_bytecode=False)]
|
|
155
|
+
if len(matching) > 1:
|
|
156
|
+
matching = [node for node in matching if _compiles_to(node, code, compare_bytecode=True)]
|
|
157
|
+
if len(matching) == 1:
|
|
158
|
+
lambdas = matching
|
|
159
|
+
if not lambdas:
|
|
160
|
+
return None
|
|
161
|
+
# Lambdas that start on the same line and compile identically share
|
|
162
|
+
# one fingerprint covering all of them.
|
|
163
|
+
return "|".join(_canonical(node) for node in lambdas)
|
|
164
|
+
definitions = [
|
|
165
|
+
node for node in ast.walk(tree)
|
|
166
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and node.name == function.__name__
|
|
167
|
+
and min([node.lineno, *(decorator.lineno for decorator in node.decorator_list)]) == first_line
|
|
168
|
+
]
|
|
169
|
+
if len(definitions) != 1:
|
|
170
|
+
return None
|
|
171
|
+
return _canonical(definitions[0])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _compiles_to(node: ast.Lambda, code: Any, *, compare_bytecode: bool) -> bool:
|
|
175
|
+
"""Whether *node* compiles to *code* on this interpreter.
|
|
176
|
+
|
|
177
|
+
The candidate is compiled inside a function that binds the live code's
|
|
178
|
+
free variables, so names resolve the same way. Only the choice of node
|
|
179
|
+
uses bytecode, which is valid within one interpreter; the fingerprint
|
|
180
|
+
hashes the node's AST, so it stays portable.
|
|
181
|
+
"""
|
|
182
|
+
free = list(code.co_freevars)
|
|
183
|
+
lines = ["def __dw_outer__():"]
|
|
184
|
+
if free:
|
|
185
|
+
lines.append(f" {' = '.join(free)} = None")
|
|
186
|
+
lines.append(f" return ({ast.unparse(node)})")
|
|
187
|
+
try:
|
|
188
|
+
outer = compile("\n".join(lines), "<condition-fingerprint>", "exec")
|
|
189
|
+
except SyntaxError:
|
|
190
|
+
return False
|
|
191
|
+
function = next((const for const in outer.co_consts if hasattr(const, "co_code")), None)
|
|
192
|
+
candidate = next(
|
|
193
|
+
(const for const in getattr(function, "co_consts", ()) if getattr(const, "co_name", None) == "<lambda>"),
|
|
194
|
+
None,
|
|
195
|
+
)
|
|
196
|
+
return candidate is not None and (
|
|
197
|
+
(not compare_bytecode or candidate.co_code == code.co_code)
|
|
198
|
+
and candidate.co_argcount == code.co_argcount
|
|
199
|
+
and candidate.co_names == code.co_names
|
|
200
|
+
and candidate.co_varnames == code.co_varnames
|
|
201
|
+
and candidate.co_freevars == code.co_freevars
|
|
202
|
+
and _plain_constants(candidate) == _plain_constants(code)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _plain_constants(code: Any) -> list[Any]:
|
|
207
|
+
"""Constants other than nested code and the qualnames Python 3.10 stores for them."""
|
|
208
|
+
return [
|
|
209
|
+
const for const in code.co_consts
|
|
210
|
+
if not hasattr(const, "co_code") and not (isinstance(const, str) and "<locals>" in const)
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _canonical(value: Any) -> str:
|
|
215
|
+
if isinstance(value, ast.AST):
|
|
216
|
+
parts = [type(value).__name__]
|
|
217
|
+
for name in value._fields:
|
|
218
|
+
if name in _SKIPPED_FIELDS:
|
|
219
|
+
continue
|
|
220
|
+
item = getattr(value, name, None)
|
|
221
|
+
if item is None or item == []:
|
|
222
|
+
continue
|
|
223
|
+
parts.append(f"{name}={_canonical(item)}")
|
|
224
|
+
return "(" + " ".join(parts) + ")"
|
|
225
|
+
if isinstance(value, list):
|
|
226
|
+
return "[" + ",".join(_canonical(item) for item in value) + "]"
|
|
227
|
+
if value is None or value is Ellipsis or isinstance(value, str | bytes | int | float | complex | bool):
|
|
228
|
+
return f"{type(value).__name__}:{value!r}"
|
|
229
|
+
return type(value).__name__
|
|
@@ -42,6 +42,8 @@ from ._cancellation_scope_history import (
|
|
|
42
42
|
CommittedCancellationScopeHistory,
|
|
43
43
|
ScopeBoundary,
|
|
44
44
|
)
|
|
45
|
+
from ._condition_fingerprint import fingerprint as _stable_condition_fingerprint
|
|
46
|
+
from ._condition_fingerprint import fingerprint_verdict, is_legacy
|
|
45
47
|
from ._cooperative_cancellation import CancellationDelivery, read_cancellation_history
|
|
46
48
|
from ._failure_reporting import exception_message
|
|
47
49
|
from ._typed_values import decode_payload_value, to_payload_values, validate_annotation
|
|
@@ -1720,28 +1722,9 @@ class CancelDurableOperation:
|
|
|
1720
1722
|
}
|
|
1721
1723
|
|
|
1722
1724
|
|
|
1723
|
-
def _condition_predicate_fingerprint(predicate: Callable[[], bool]) -> str:
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
h.update(f"{getattr(predicate, '__module__', '')}\0".encode())
|
|
1727
|
-
h.update(f"{getattr(predicate, '__qualname__', '')}\0".encode())
|
|
1728
|
-
|
|
1729
|
-
code = getattr(predicate, "__code__", None)
|
|
1730
|
-
if code is None:
|
|
1731
|
-
h.update(repr(predicate).encode())
|
|
1732
|
-
else:
|
|
1733
|
-
h.update(repr((
|
|
1734
|
-
code.co_argcount,
|
|
1735
|
-
code.co_posonlyargcount,
|
|
1736
|
-
code.co_kwonlyargcount,
|
|
1737
|
-
code.co_code,
|
|
1738
|
-
code.co_consts,
|
|
1739
|
-
code.co_names,
|
|
1740
|
-
code.co_varnames,
|
|
1741
|
-
code.co_freevars,
|
|
1742
|
-
)).encode())
|
|
1743
|
-
|
|
1744
|
-
return f"sha256:{h.hexdigest()}"
|
|
1725
|
+
def _condition_predicate_fingerprint(predicate: Callable[[], bool]) -> str | None:
|
|
1726
|
+
"""The stable v2 predicate fingerprint, or None without source (#258)."""
|
|
1727
|
+
return _stable_condition_fingerprint(predicate)
|
|
1745
1728
|
|
|
1746
1729
|
|
|
1747
1730
|
Command = (
|
|
@@ -6017,10 +6000,7 @@ def _replay_state(
|
|
|
6017
6000
|
)
|
|
6018
6001
|
|
|
6019
6002
|
opened_fingerprint = opened.get("condition_definition_fingerprint")
|
|
6020
|
-
if (
|
|
6021
|
-
isinstance(opened_fingerprint, str)
|
|
6022
|
-
and cmd.condition_definition_fingerprint != opened_fingerprint
|
|
6023
|
-
):
|
|
6003
|
+
if _fingerprint_verdict(opened, cmd) == "mismatch":
|
|
6024
6004
|
return NonDeterministicReplayError(
|
|
6025
6005
|
step.workflow_sequence, "condition wait", step.event_types,
|
|
6026
6006
|
detail=(
|
|
@@ -6033,7 +6013,19 @@ def _replay_state(
|
|
|
6033
6013
|
|
|
6034
6014
|
return None
|
|
6035
6015
|
|
|
6036
|
-
def
|
|
6016
|
+
def _fingerprint_verdict(opened: Mapping[str, Any], cmd: WaitCondition) -> str:
|
|
6017
|
+
return fingerprint_verdict(
|
|
6018
|
+
opened.get("condition_definition_fingerprint"), cmd.condition_definition_fingerprint, cmd.predicate,
|
|
6019
|
+
)
|
|
6020
|
+
|
|
6021
|
+
def _same_logical_condition_wait(
|
|
6022
|
+
opened: Mapping[str, Any],
|
|
6023
|
+
cmd: WaitCondition,
|
|
6024
|
+
*,
|
|
6025
|
+
chain_first: Mapping[str, Any] | None = None,
|
|
6026
|
+
chain_current: Mapping[str, Any] | None = None,
|
|
6027
|
+
next_index: int | None = None,
|
|
6028
|
+
) -> bool:
|
|
6037
6029
|
if _condition_wait_mismatch(opened, cmd) is not None:
|
|
6038
6030
|
return False
|
|
6039
6031
|
|
|
@@ -6042,10 +6034,33 @@ def _replay_state(
|
|
|
6042
6034
|
return True
|
|
6043
6035
|
|
|
6044
6036
|
opened_fingerprint = opened.get("condition_definition_fingerprint")
|
|
6037
|
+
if not isinstance(opened_fingerprint, str) or not opened_fingerprint:
|
|
6038
|
+
# A keyless wait without source has no identity beyond call order.
|
|
6039
|
+
# Within its own chain (both opens recorded without a fingerprint)
|
|
6040
|
+
# it is the same wait, as 2.14.0 linked it by its process-local v1.
|
|
6041
|
+
first_fingerprint = (
|
|
6042
|
+
chain_first.get("condition_definition_fingerprint") if chain_first is not None else "absent"
|
|
6043
|
+
)
|
|
6044
|
+
return (
|
|
6045
|
+
cmd.condition_definition_fingerprint is None and not cmd.condition_key
|
|
6046
|
+
and chain_first is not None and not first_fingerprint
|
|
6047
|
+
)
|
|
6048
|
+
verdict = _fingerprint_verdict(opened, cmd)
|
|
6049
|
+
if verdict == "match":
|
|
6050
|
+
return True
|
|
6051
|
+
if verdict != "unverifiable":
|
|
6052
|
+
return False
|
|
6053
|
+
# A keyless wait whose fingerprint cannot be checked (a v1 value from
|
|
6054
|
+
# another Python version or process, #258) links only within its own
|
|
6055
|
+
# chain: the same recorded v1 value as the chain's first open, or a
|
|
6056
|
+
# duplicate opened while the current wait was still open (#253). It
|
|
6057
|
+
# never binds a recorded wait to another call.
|
|
6058
|
+
first_fingerprint = chain_first.get("condition_definition_fingerprint") if chain_first is not None else None
|
|
6059
|
+
if is_legacy(opened_fingerprint) and opened_fingerprint == first_fingerprint:
|
|
6060
|
+
return True
|
|
6045
6061
|
return (
|
|
6046
|
-
|
|
6047
|
-
and
|
|
6048
|
-
and opened_fingerprint == cmd.condition_definition_fingerprint
|
|
6062
|
+
chain_current is not None and next_index is not None
|
|
6063
|
+
and _opened_before_terminal(next_index, chain_current)
|
|
6049
6064
|
)
|
|
6050
6065
|
|
|
6051
6066
|
result_cursor = 0
|
|
@@ -6111,6 +6126,10 @@ def _replay_state(
|
|
|
6111
6126
|
pending: list[Command] = []
|
|
6112
6127
|
advanced_cmd: Any = None
|
|
6113
6128
|
terminal_condition_reopen_cmd: WaitCondition | None = None
|
|
6129
|
+
# The first open of the chain a terminal reopen cleanup belongs to (#258).
|
|
6130
|
+
terminal_condition_chain_first: Mapping[str, Any] | None = None
|
|
6131
|
+
# Its terminal position proves whether a different-process duplicate belongs.
|
|
6132
|
+
terminal_condition_chain_current: Mapping[str, Any] | None = None
|
|
6114
6133
|
version_decisions: dict[str, int] = {}
|
|
6115
6134
|
|
|
6116
6135
|
def _version_error(detail: str, step: _RecordedStep | None = None) -> NonDeterministicReplayError:
|
|
@@ -6686,7 +6705,10 @@ def _replay_state(
|
|
|
6686
6705
|
return
|
|
6687
6706
|
while wait_yield_count < len(wait_opened):
|
|
6688
6707
|
opened = wait_opened[wait_yield_count]
|
|
6689
|
-
if not _same_logical_condition_wait(
|
|
6708
|
+
if not _same_logical_condition_wait(
|
|
6709
|
+
opened, terminal_condition_reopen_cmd, chain_first=terminal_condition_chain_first,
|
|
6710
|
+
chain_current=terminal_condition_chain_current, next_index=wait_yield_count,
|
|
6711
|
+
):
|
|
6690
6712
|
break
|
|
6691
6713
|
opened_id = opened.get("condition_wait_id")
|
|
6692
6714
|
if not isinstance(opened_id, str):
|
|
@@ -6928,9 +6950,14 @@ def _replay_state(
|
|
|
6928
6950
|
expected = ({"delay_seconds": leaf.delay_seconds} if isinstance(leaf, StartTimer) else {
|
|
6929
6951
|
"condition_key": leaf.condition_key,
|
|
6930
6952
|
"timeout_seconds": leaf.timeout_seconds,
|
|
6931
|
-
"condition_definition_fingerprint": leaf.condition_definition_fingerprint,
|
|
6932
6953
|
})
|
|
6933
|
-
|
|
6954
|
+
fingerprint_changed = isinstance(leaf, WaitCondition) and fingerprint_verdict(
|
|
6955
|
+
payload.get("condition_definition_fingerprint"), leaf.condition_definition_fingerprint,
|
|
6956
|
+
leaf.predicate,
|
|
6957
|
+
) == "mismatch"
|
|
6958
|
+
if fingerprint_changed or any(
|
|
6959
|
+
field in payload and payload[field] != actual for field, actual in expected.items()
|
|
6960
|
+
):
|
|
6934
6961
|
raise NonDeterministicReplayError(
|
|
6935
6962
|
sequence, "original group member", [event_type],
|
|
6936
6963
|
detail="scoped group changed its original timer or condition definition",
|
|
@@ -7054,7 +7081,7 @@ def _replay_state(
|
|
|
7054
7081
|
return terminal is not None and wait_opened_event_indexes[next_index] < terminal
|
|
7055
7082
|
|
|
7056
7083
|
def _reopened_with_remaining_timeout(command: WaitCondition, first_index: int) -> WaitCondition:
|
|
7057
|
-
"""Reopen a logical wait with
|
|
7084
|
+
"""Reopen a logical wait with its first fingerprint and remaining time.
|
|
7058
7085
|
|
|
7059
7086
|
The deadline is fixed when the wait first opens, as in embedded
|
|
7060
7087
|
Workflow: the first ConditionWaitOpened time plus the authored
|
|
@@ -7063,14 +7090,20 @@ def _replay_state(
|
|
|
7063
7090
|
history time, rounded up and clamped to 0..authored (the sdk-php#269
|
|
7064
7091
|
rule). Without recorded times the authored timeout is kept.
|
|
7065
7092
|
"""
|
|
7066
|
-
|
|
7067
|
-
if authored is None or first_index >= len(wait_opened):
|
|
7093
|
+
if first_index >= len(wait_opened):
|
|
7068
7094
|
return command
|
|
7095
|
+
reopened = copy(command)
|
|
7096
|
+
# A reopen sends the fingerprint its first open recorded, so a wait
|
|
7097
|
+
# first opened under v1 (2.14.0 and earlier) keeps v1 across the
|
|
7098
|
+
# upgrade and links v1 to v1, never v1 to v2 (#258).
|
|
7099
|
+
recorded = wait_opened[first_index].get("condition_definition_fingerprint")
|
|
7100
|
+
if isinstance(recorded, str) and recorded:
|
|
7101
|
+
reopened.condition_definition_fingerprint = recorded
|
|
7102
|
+
authored = command.timeout_seconds
|
|
7069
7103
|
opened_at = wait_opened_times[first_index]
|
|
7070
|
-
if opened_at is None or history_time is None:
|
|
7071
|
-
return
|
|
7104
|
+
if authored is None or opened_at is None or history_time is None:
|
|
7105
|
+
return reopened
|
|
7072
7106
|
remaining = math.ceil(authored - (history_time - opened_at).total_seconds())
|
|
7073
|
-
reopened = copy(command)
|
|
7074
7107
|
reopened.timeout_seconds = min(authored, max(0, remaining))
|
|
7075
7108
|
return reopened
|
|
7076
7109
|
|
|
@@ -7730,7 +7763,10 @@ def _replay_state(
|
|
|
7730
7763
|
has_reopened_same_wait = (
|
|
7731
7764
|
opened is not None
|
|
7732
7765
|
and next_wait_index < len(wait_opened)
|
|
7733
|
-
and _same_logical_condition_wait(
|
|
7766
|
+
and _same_logical_condition_wait(
|
|
7767
|
+
wait_opened[next_wait_index], cmd, chain_first=wait_opened[first_open_index],
|
|
7768
|
+
chain_current=opened, next_index=next_wait_index,
|
|
7769
|
+
)
|
|
7734
7770
|
)
|
|
7735
7771
|
if resolution == "timed_out":
|
|
7736
7772
|
if has_reopened_same_wait and _opened_before_terminal(next_wait_index, opened):
|
|
@@ -7769,6 +7805,8 @@ def _replay_state(
|
|
|
7769
7805
|
if has_reopened_same_wait and consumed_reopen_for_current_wait
|
|
7770
7806
|
else None
|
|
7771
7807
|
)
|
|
7808
|
+
terminal_condition_chain_first = wait_opened[first_open_index]
|
|
7809
|
+
terminal_condition_chain_current = opened
|
|
7772
7810
|
next_value = True
|
|
7773
7811
|
wait_yield_count += 1
|
|
7774
7812
|
break
|
|
@@ -7778,6 +7816,8 @@ def _replay_state(
|
|
|
7778
7816
|
if has_reopened_same_wait and consumed_reopen_for_current_wait
|
|
7779
7817
|
else None
|
|
7780
7818
|
)
|
|
7819
|
+
terminal_condition_chain_first = wait_opened[first_open_index]
|
|
7820
|
+
terminal_condition_chain_current = opened
|
|
7781
7821
|
next_value = True
|
|
7782
7822
|
wait_yield_count += 1
|
|
7783
7823
|
break
|
{durable_workflow-2.14.0 → durable_workflow-2.14.1}/src/durable_workflow.egg-info/SOURCES.txt
RENAMED
|
@@ -7,6 +7,7 @@ src/durable_workflow/_activity_process.py
|
|
|
7
7
|
src/durable_workflow/_avro.py
|
|
8
8
|
src/durable_workflow/_cancellation_scope.py
|
|
9
9
|
src/durable_workflow/_cancellation_scope_history.py
|
|
10
|
+
src/durable_workflow/_condition_fingerprint.py
|
|
10
11
|
src/durable_workflow/_cooperative_cancellation.py
|
|
11
12
|
src/durable_workflow/_failure_reporting.py
|
|
12
13
|
src/durable_workflow/_metric_routes.py
|
|
@@ -78,6 +79,7 @@ tests/test_committed_cancellation_scope_descendants.py
|
|
|
78
79
|
tests/test_committed_cancellation_scope_groups.py
|
|
79
80
|
tests/test_committed_cancellation_scope_history.py
|
|
80
81
|
tests/test_committed_cancellation_scope_replay.py
|
|
82
|
+
tests/test_condition_fingerprint.py
|
|
81
83
|
tests/test_condition_replay_drift.py
|
|
82
84
|
tests/test_control_plane_parity_fixtures.py
|
|
83
85
|
tests/test_cooperative_cancellation.py
|
|
@@ -60,10 +60,15 @@ async def response_server(*, delay: float | None) -> AsyncIterator[str]:
|
|
|
60
60
|
@pytest.mark.asyncio
|
|
61
61
|
@pytest.mark.parametrize("operation", ["control", "discovery", "worker"])
|
|
62
62
|
async def test_configured_timeout_bounds_a_stalled_response(operation: str) -> None:
|
|
63
|
+
async def allow_connection_setup(request: httpx.Request) -> None:
|
|
64
|
+
# Exercise the configured read budget even when cold TCP setup is slow.
|
|
65
|
+
request.extensions["timeout"]["connect"] = 1.0
|
|
66
|
+
|
|
63
67
|
async with (
|
|
64
68
|
response_server(delay=None) as endpoint,
|
|
65
69
|
Client(endpoint, timeout=0.05, retry_policy=TransportRetryPolicy(max_attempts=1)) as client,
|
|
66
70
|
):
|
|
71
|
+
client._http.event_hooks["request"].append(allow_connection_setup)
|
|
67
72
|
if operation == "control":
|
|
68
73
|
request = client.health()
|
|
69
74
|
elif operation == "discovery":
|