durable-workflow 2.12.4__tar.gz → 2.13.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.12.4/src/durable_workflow.egg-info → durable_workflow-2.13.1}/PKG-INFO +1 -1
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/pyproject.toml +3 -3
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/__init__.py +4 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/history_bundle_verify.py +32 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/worker.py +243 -39
- durable_workflow-2.13.1/src/durable_workflow/worker_status.py +385 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/workflow.py +13 -3
- {durable_workflow-2.12.4 → durable_workflow-2.13.1/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow.egg-info/SOURCES.txt +3 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_cancellation_remaining_time.py +19 -0
- durable_workflow-2.13.1/tests/test_history_bundle_pruned.py +132 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_history_event_contract.py +26 -1
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_replay_clock.py +41 -1
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_replay_regression_corpus.py +2 -0
- durable_workflow-2.13.1/tests/test_worker_status.py +574 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/LICENSE +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/README.md +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/setup.cfg +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_activity_lease.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_failure_reporting.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_metric_routes.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_poll_transport.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/server_compatibility.json +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/telemetry.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/telemetry_carrier.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_activity_heartbeat_authority.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_activity_lease_authority.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_activity_lease_renewal.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_activity_lease_worker.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_activity_retry_info.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_client.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_client_telemetry.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_empty_callback_failure.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_errors.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_failure_reporting.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_generator_lifecycle.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_history_bundle_schema2.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_invocable.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_local_failure_reporting.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_metric_routes.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_metrics.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_poll_failure_backoff.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_poll_transport.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_queries.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_replay.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_schedules.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_sdk_metrics_contract.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_serializer.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_signals.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_sleep.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_sync.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_sync_activities.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_telemetry.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_telemetry_carrier.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_testing_failures.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_testing_nexus.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_threaded_heartbeat_example.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_tls_trust.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_transport_diagnostics.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_updates.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_worker.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_worker_deregistration.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_worker_observation.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_worker_telemetry_wire.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_workflow_entry_arguments.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_workflow_handler_validation.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_workflow_identity.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.1}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.12.4 → durable_workflow-2.13.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.
|
|
7
|
+
version = "2.13.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.
|
|
83
|
-
registry-version = "2.
|
|
82
|
+
product-train = "2.13.1"
|
|
83
|
+
registry-version = "2.13.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"
|
|
@@ -225,6 +225,7 @@ from .serializer import (
|
|
|
225
225
|
)
|
|
226
226
|
from .worker import Worker
|
|
227
227
|
from .worker_session import WorkerSession, WorkerSessionOptions
|
|
228
|
+
from .worker_status import WorkerRoleStatus, WorkerState, WorkerStatus
|
|
228
229
|
from .workflow import (
|
|
229
230
|
ActivityRetryPolicy,
|
|
230
231
|
ChildWorkflowRetryPolicy,
|
|
@@ -314,6 +315,9 @@ __all__ = [
|
|
|
314
315
|
"Worker",
|
|
315
316
|
"WorkerSession",
|
|
316
317
|
"WorkerSessionOptions",
|
|
318
|
+
"WorkerRoleStatus",
|
|
319
|
+
"WorkerState",
|
|
320
|
+
"WorkerStatus",
|
|
317
321
|
"WorkerInterceptor",
|
|
318
322
|
"WorkflowCommandResult",
|
|
319
323
|
"WorkflowExecution",
|
{durable_workflow-2.12.4 → durable_workflow-2.13.1}/src/durable_workflow/history_bundle_verify.py
RENAMED
|
@@ -100,6 +100,7 @@ def verify_bundle(bundle: Any, signing_key: str | None = None) -> dict[str, Any]
|
|
|
100
100
|
|
|
101
101
|
_check_envelope(bundle, findings)
|
|
102
102
|
_check_workflow(bundle, findings)
|
|
103
|
+
_check_retained_details(bundle, findings)
|
|
103
104
|
_check_history_events(bundle, findings)
|
|
104
105
|
_check_commands(bundle, findings)
|
|
105
106
|
_check_payload_manifest(bundle, findings)
|
|
@@ -342,6 +343,37 @@ def _php_integer_key(key: str) -> bool:
|
|
|
342
343
|
return -(2**63) <= int(key) <= 2**63 - 1
|
|
343
344
|
|
|
344
345
|
|
|
346
|
+
def _check_retained_details(bundle: Mapping[str, Any], findings: list[dict[str, Any]]) -> None:
|
|
347
|
+
"""Fail a run whose details retention cleanup pruned.
|
|
348
|
+
|
|
349
|
+
Such a bundle has no history to replay or import, whatever its other
|
|
350
|
+
fields say. Bundles without ``workflow.details_state`` (older exports) and
|
|
351
|
+
bundles marked ``retained`` are unaffected.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
workflow = bundle.get("workflow")
|
|
355
|
+
if not isinstance(workflow, Mapping) or workflow.get("details_state") != "pruned":
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
pruned_at = _string_or_none(workflow.get("details_pruned_at"))
|
|
359
|
+
findings.append(
|
|
360
|
+
_finding(
|
|
361
|
+
"history.details_pruned",
|
|
362
|
+
SEVERITY_ERROR,
|
|
363
|
+
(
|
|
364
|
+
"Retention cleanup pruned this run's details"
|
|
365
|
+
f"{'' if pruned_at is None else ' at ' + pruned_at}"
|
|
366
|
+
"; the bundle has no history to replay or import."
|
|
367
|
+
),
|
|
368
|
+
{
|
|
369
|
+
"details_pruned_at": pruned_at,
|
|
370
|
+
"history_complete": bundle.get("history_complete"),
|
|
371
|
+
},
|
|
372
|
+
path="workflow.details_state",
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
345
377
|
def _check_history_events(bundle: Mapping[str, Any], findings: list[dict[str, Any]]) -> None:
|
|
346
378
|
events = bundle.get("history_events")
|
|
347
379
|
if not isinstance(events, list):
|
|
@@ -22,6 +22,7 @@ import hashlib
|
|
|
22
22
|
import inspect
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
|
+
import math
|
|
25
26
|
import pickle
|
|
26
27
|
import random
|
|
27
28
|
import sys
|
|
@@ -68,6 +69,7 @@ from .client import (
|
|
|
68
69
|
WorkflowExecution,
|
|
69
70
|
_protocol_version_from_env,
|
|
70
71
|
_supports_cooperative_cancellation_protocol,
|
|
72
|
+
_worker_poll_http_timeout,
|
|
71
73
|
)
|
|
72
74
|
from .errors import (
|
|
73
75
|
ActivityCancelled,
|
|
@@ -108,6 +110,12 @@ from .retry_policy import _worker_storage_admission_stop
|
|
|
108
110
|
from .telemetry import ExecutionTelemetry, current_execution_telemetry
|
|
109
111
|
from .telemetry_carrier import identity as observability_identity
|
|
110
112
|
from .worker_session import WorkerSession, WorkerSessionOptions
|
|
113
|
+
from .worker_status import (
|
|
114
|
+
DEFAULT_EVENT_LOOP_STALL_SECONDS,
|
|
115
|
+
EVENT_LOOP_TICK_SECONDS,
|
|
116
|
+
WorkerStatus,
|
|
117
|
+
_WorkerStatusTracker,
|
|
118
|
+
)
|
|
111
119
|
from .workflow import (
|
|
112
120
|
Command,
|
|
113
121
|
FailWorkflow,
|
|
@@ -233,6 +241,15 @@ def _is_storage_admission_error(error: BaseException) -> bool:
|
|
|
233
241
|
)
|
|
234
242
|
|
|
235
243
|
|
|
244
|
+
def _positive_seconds(value: object) -> bool:
|
|
245
|
+
return (
|
|
246
|
+
isinstance(value, (int, float))
|
|
247
|
+
and not isinstance(value, bool)
|
|
248
|
+
and math.isfinite(value)
|
|
249
|
+
and value > 0
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
236
253
|
def _poll_error_summary(error: BaseException) -> str:
|
|
237
254
|
error_class = type(error).__name__
|
|
238
255
|
if isinstance(error, Unauthorized) or isinstance(error, ServerError) and error.status == 401:
|
|
@@ -1209,6 +1226,8 @@ class Worker:
|
|
|
1209
1226
|
self._query_thread: threading.Thread | None = None
|
|
1210
1227
|
self._query_thread_loop: asyncio.AbstractEventLoop | None = None
|
|
1211
1228
|
self._query_thread_task: asyncio.Task[None] | None = None
|
|
1229
|
+
self._status = _WorkerStatusTracker()
|
|
1230
|
+
self._status_tick_task: asyncio.Task[None] | None = None
|
|
1212
1231
|
# In-flight slot accounting feeds the periodic heartbeat so operators
|
|
1213
1232
|
# see free-slot counts without the worker having to re-derive them at
|
|
1214
1233
|
# shutdown. A workflow reservation covers the multiplexed long poll,
|
|
@@ -1514,6 +1533,7 @@ class Worker:
|
|
|
1514
1533
|
advertised = ack.get("heartbeat_interval_seconds")
|
|
1515
1534
|
if isinstance(advertised, int) and advertised > 0:
|
|
1516
1535
|
self._heartbeat_interval = float(advertised)
|
|
1536
|
+
self._status.registered()
|
|
1517
1537
|
log.info("worker %s registered on %s", self.worker_id, self.task_queue)
|
|
1518
1538
|
|
|
1519
1539
|
async def _run_workflow_task(self, task: dict[str, Any]) -> list[dict[str, Any]] | None:
|
|
@@ -3793,6 +3813,7 @@ class Worker:
|
|
|
3793
3813
|
return
|
|
3794
3814
|
try:
|
|
3795
3815
|
poll_start = time.perf_counter()
|
|
3816
|
+
self._status.request_started("workflow_poll")
|
|
3796
3817
|
task = await self.client.poll_workflow_task(
|
|
3797
3818
|
worker_id=self.worker_id,
|
|
3798
3819
|
task_queue=self.task_queue,
|
|
@@ -3802,15 +3823,18 @@ class Worker:
|
|
|
3802
3823
|
history_page_size=WORKFLOW_HISTORY_PAGE_SIZE,
|
|
3803
3824
|
)
|
|
3804
3825
|
except asyncio.CancelledError:
|
|
3826
|
+
self._status.request_abandoned("workflow_poll")
|
|
3805
3827
|
self._release_workflow_capacity()
|
|
3806
3828
|
raise
|
|
3807
3829
|
except Exception as e:
|
|
3808
3830
|
self._release_workflow_capacity()
|
|
3809
3831
|
if self._stop.is_set():
|
|
3832
|
+
self._status.request_abandoned("workflow_poll")
|
|
3810
3833
|
return
|
|
3834
|
+
delay = _poll_capacity_delay(e, "workflow_task", self.task_queue)
|
|
3835
|
+
self._record_poll_status("workflow_poll", e, answered=delay is not None)
|
|
3811
3836
|
if _is_storage_admission_error(e):
|
|
3812
3837
|
raise
|
|
3813
|
-
delay = _poll_capacity_delay(e, "workflow_task", self.task_queue)
|
|
3814
3838
|
if delay is not None:
|
|
3815
3839
|
self._record_poll_metrics("workflow", "backpressure", time.perf_counter() - poll_start)
|
|
3816
3840
|
log.debug("workflow poll capacity backpressure; retrying in %d s", delay)
|
|
@@ -3820,6 +3844,7 @@ class Worker:
|
|
|
3820
3844
|
await asyncio.sleep(failures.retry_delay(e, "workflow"))
|
|
3821
3845
|
continue
|
|
3822
3846
|
failures.reset()
|
|
3847
|
+
self._status.request_succeeded("workflow_poll")
|
|
3823
3848
|
if task is None:
|
|
3824
3849
|
self._release_workflow_capacity()
|
|
3825
3850
|
self._record_poll_metrics("workflow", "empty", time.perf_counter() - poll_start)
|
|
@@ -3905,6 +3930,7 @@ class Worker:
|
|
|
3905
3930
|
return
|
|
3906
3931
|
try:
|
|
3907
3932
|
poll_start = time.perf_counter()
|
|
3933
|
+
self._status.request_started("activity_poll")
|
|
3908
3934
|
task = await self.client.poll_activity_task(
|
|
3909
3935
|
worker_id=self.worker_id,
|
|
3910
3936
|
task_queue=self.task_queue,
|
|
@@ -3912,15 +3938,18 @@ class Worker:
|
|
|
3912
3938
|
build_id=self.build_id,
|
|
3913
3939
|
)
|
|
3914
3940
|
except asyncio.CancelledError:
|
|
3941
|
+
self._status.request_abandoned("activity_poll")
|
|
3915
3942
|
self._act_semaphore.release()
|
|
3916
3943
|
raise
|
|
3917
3944
|
except Exception as e:
|
|
3918
3945
|
self._act_semaphore.release()
|
|
3919
3946
|
if self._stop.is_set():
|
|
3947
|
+
self._status.request_abandoned("activity_poll")
|
|
3920
3948
|
return
|
|
3949
|
+
delay = _poll_capacity_delay(e, "activity_task", self.task_queue)
|
|
3950
|
+
self._record_poll_status("activity_poll", e, answered=delay is not None)
|
|
3921
3951
|
if _is_storage_admission_error(e):
|
|
3922
3952
|
raise
|
|
3923
|
-
delay = _poll_capacity_delay(e, "activity_task", self.task_queue)
|
|
3924
3953
|
if delay is not None:
|
|
3925
3954
|
self._record_poll_metrics("activity", "backpressure", time.perf_counter() - poll_start)
|
|
3926
3955
|
log.debug("activity poll capacity backpressure; retrying in %d s", delay)
|
|
@@ -3930,6 +3959,7 @@ class Worker:
|
|
|
3930
3959
|
await asyncio.sleep(failures.retry_delay(e, "activity"))
|
|
3931
3960
|
continue
|
|
3932
3961
|
failures.reset()
|
|
3962
|
+
self._status.request_succeeded("activity_poll")
|
|
3933
3963
|
if task is None:
|
|
3934
3964
|
self._act_semaphore.release()
|
|
3935
3965
|
self._record_poll_metrics("activity", "empty", time.perf_counter() - poll_start)
|
|
@@ -3965,20 +3995,26 @@ class Worker:
|
|
|
3965
3995
|
):
|
|
3966
3996
|
try:
|
|
3967
3997
|
poll_start = time.perf_counter()
|
|
3998
|
+
self._status.request_started("query_poll")
|
|
3968
3999
|
task = await client.poll_query_task(
|
|
3969
4000
|
worker_id=self.worker_id,
|
|
3970
4001
|
task_queue=self.task_queue,
|
|
3971
4002
|
timeout=self._poll_timeout,
|
|
3972
4003
|
build_id=self.build_id,
|
|
3973
4004
|
)
|
|
4005
|
+
except asyncio.CancelledError:
|
|
4006
|
+
self._status.request_abandoned("query_poll")
|
|
4007
|
+
raise
|
|
3974
4008
|
except Exception as e:
|
|
3975
4009
|
if self._stop.is_set() or (
|
|
3976
4010
|
query_thread_stop is not None and query_thread_stop.is_set()
|
|
3977
4011
|
):
|
|
4012
|
+
self._status.request_abandoned("query_poll")
|
|
3978
4013
|
return
|
|
4014
|
+
delay = _poll_capacity_delay(e, "query_task", self.task_queue)
|
|
4015
|
+
self._record_poll_status("query_poll", e, answered=delay is not None)
|
|
3979
4016
|
if _is_storage_admission_error(e):
|
|
3980
4017
|
raise
|
|
3981
|
-
delay = _poll_capacity_delay(e, "query_task", self.task_queue)
|
|
3982
4018
|
if delay is not None:
|
|
3983
4019
|
self._record_poll_metrics("query", "backpressure", time.perf_counter() - poll_start)
|
|
3984
4020
|
log.debug("query poll capacity backpressure; retrying in %d s", delay)
|
|
@@ -3988,6 +4024,7 @@ class Worker:
|
|
|
3988
4024
|
await asyncio.sleep(failures.retry_delay(e, "query"))
|
|
3989
4025
|
continue
|
|
3990
4026
|
failures.reset()
|
|
4027
|
+
self._status.request_succeeded("query_poll")
|
|
3991
4028
|
if task is None:
|
|
3992
4029
|
self._record_poll_metrics("query", "empty", time.perf_counter() - poll_start)
|
|
3993
4030
|
if self._stop.is_set() or (
|
|
@@ -4231,6 +4268,8 @@ class Worker:
|
|
|
4231
4268
|
log.exception("query task poller thread was cancelled unexpectedly")
|
|
4232
4269
|
except Exception:
|
|
4233
4270
|
log.exception("query task poller thread stopped unexpectedly")
|
|
4271
|
+
finally:
|
|
4272
|
+
self._status.role_exited("query_poll")
|
|
4234
4273
|
|
|
4235
4274
|
@_with_storage_admission_retries
|
|
4236
4275
|
async def _query_task_thread_main(self) -> None:
|
|
@@ -4274,24 +4313,12 @@ class Worker:
|
|
|
4274
4313
|
async def run(self) -> None:
|
|
4275
4314
|
"""Register the worker and poll until `stop()` is called or the task is cancelled."""
|
|
4276
4315
|
self._begin_run()
|
|
4277
|
-
|
|
4278
|
-
await self._register()
|
|
4279
|
-
finally:
|
|
4280
|
-
self._registration_done.set()
|
|
4316
|
+
await self._register_for_run()
|
|
4281
4317
|
|
|
4282
4318
|
try:
|
|
4283
4319
|
if self._stop.is_set():
|
|
4284
4320
|
return
|
|
4285
|
-
loops =
|
|
4286
|
-
if self.workflows:
|
|
4287
|
-
loops.append(asyncio.create_task(self._poll_workflow_tasks()))
|
|
4288
|
-
if self.activities:
|
|
4289
|
-
loops.append(asyncio.create_task(self._poll_activity_tasks()))
|
|
4290
|
-
if self.workflows and self._query_tasks_supported:
|
|
4291
|
-
if self._can_clone_client_for_query_tasks():
|
|
4292
|
-
self._start_query_task_thread()
|
|
4293
|
-
else:
|
|
4294
|
-
loops.append(asyncio.create_task(self._poll_query_tasks()))
|
|
4321
|
+
loops = self._start_run_pollers()
|
|
4295
4322
|
self._poller_tasks.update(loops)
|
|
4296
4323
|
|
|
4297
4324
|
try:
|
|
@@ -4302,6 +4329,8 @@ class Worker:
|
|
|
4302
4329
|
finally:
|
|
4303
4330
|
primary_error = sys.exc_info()[1]
|
|
4304
4331
|
primary_traceback = primary_error.__traceback__ if primary_error is not None else None
|
|
4332
|
+
if isinstance(primary_error, Exception):
|
|
4333
|
+
self._status.failed("runtime")
|
|
4305
4334
|
try:
|
|
4306
4335
|
await self.stop()
|
|
4307
4336
|
except BaseException as shutdown_error:
|
|
@@ -4309,6 +4338,35 @@ class Worker:
|
|
|
4309
4338
|
raise primary_error.with_traceback(primary_traceback) from shutdown_error
|
|
4310
4339
|
raise
|
|
4311
4340
|
|
|
4341
|
+
async def _register_for_run(self) -> None:
|
|
4342
|
+
try:
|
|
4343
|
+
await self._register()
|
|
4344
|
+
except BaseException as error:
|
|
4345
|
+
if isinstance(error, Exception):
|
|
4346
|
+
self._status.failed("registration")
|
|
4347
|
+
if self._shutdown_task is None:
|
|
4348
|
+
# run() leaves no shutdown behind a failed registration.
|
|
4349
|
+
self._cancel_status_tick()
|
|
4350
|
+
raise
|
|
4351
|
+
finally:
|
|
4352
|
+
self._registration_done.set()
|
|
4353
|
+
|
|
4354
|
+
def _start_run_pollers(self) -> list[asyncio.Task[None]]:
|
|
4355
|
+
roles: list[tuple[str, Coroutine[Any, Any, None] | None]] = [("heartbeat", self._heartbeat_loop())]
|
|
4356
|
+
if self.workflows:
|
|
4357
|
+
roles.append(("workflow_poll", self._poll_workflow_tasks()))
|
|
4358
|
+
if self.activities:
|
|
4359
|
+
roles.append(("activity_poll", self._poll_activity_tasks()))
|
|
4360
|
+
query_thread = False
|
|
4361
|
+
if self.workflows and self._query_tasks_supported:
|
|
4362
|
+
query_thread = self._can_clone_client_for_query_tasks()
|
|
4363
|
+
roles.append(("query_poll", None if query_thread else self._poll_query_tasks()))
|
|
4364
|
+
self._status.roles_started(role for role, _ in roles)
|
|
4365
|
+
loops = [self._start_status_role(role, loop) for role, loop in roles if loop is not None]
|
|
4366
|
+
if query_thread:
|
|
4367
|
+
self._start_query_task_thread()
|
|
4368
|
+
return loops
|
|
4369
|
+
|
|
4312
4370
|
async def _heartbeat_loop(self) -> None:
|
|
4313
4371
|
"""Periodically refresh the server-side worker registration.
|
|
4314
4372
|
|
|
@@ -4325,22 +4383,36 @@ class Worker:
|
|
|
4325
4383
|
await asyncio.wait_for(self._stop.wait(), timeout=self._heartbeat_interval)
|
|
4326
4384
|
if self._stop.is_set():
|
|
4327
4385
|
return
|
|
4328
|
-
|
|
4329
|
-
ack = await self.client.heartbeat_worker(
|
|
4330
|
-
worker_id=self.worker_id,
|
|
4331
|
-
task_slots=self._current_task_slots(),
|
|
4332
|
-
process_metrics=self._current_process_metrics(),
|
|
4333
|
-
)
|
|
4334
|
-
except Exception as e:
|
|
4335
|
-
if _is_storage_admission_error(e) and not self._stop.is_set():
|
|
4336
|
-
raise
|
|
4337
|
-
log.warning("worker heartbeat failed: %s", e)
|
|
4338
|
-
continue
|
|
4386
|
+
ack = await self._send_heartbeat()
|
|
4339
4387
|
if isinstance(ack, dict):
|
|
4340
4388
|
advertised = ack.get("heartbeat_interval_seconds")
|
|
4341
4389
|
if isinstance(advertised, int) and advertised > 0:
|
|
4342
4390
|
self._heartbeat_interval = float(advertised)
|
|
4343
4391
|
|
|
4392
|
+
async def _send_heartbeat(self) -> Any:
|
|
4393
|
+
"""Send one heartbeat; return its acknowledgement, or ``None`` after a logged failure."""
|
|
4394
|
+
self._status.request_started("heartbeat")
|
|
4395
|
+
try:
|
|
4396
|
+
ack = await self.client.heartbeat_worker(
|
|
4397
|
+
worker_id=self.worker_id,
|
|
4398
|
+
task_slots=self._current_task_slots(),
|
|
4399
|
+
process_metrics=self._current_process_metrics(),
|
|
4400
|
+
)
|
|
4401
|
+
except asyncio.CancelledError:
|
|
4402
|
+
self._status.request_abandoned("heartbeat")
|
|
4403
|
+
raise
|
|
4404
|
+
except Exception as e:
|
|
4405
|
+
if self._stop.is_set():
|
|
4406
|
+
self._status.request_abandoned("heartbeat")
|
|
4407
|
+
else:
|
|
4408
|
+
self._status.request_failed("heartbeat", e)
|
|
4409
|
+
if _is_storage_admission_error(e) and not self._stop.is_set():
|
|
4410
|
+
raise
|
|
4411
|
+
log.warning("worker heartbeat failed: %s", e)
|
|
4412
|
+
return None
|
|
4413
|
+
self._status.request_succeeded("heartbeat")
|
|
4414
|
+
return ack
|
|
4415
|
+
|
|
4344
4416
|
def _current_task_slots(self) -> dict[str, int]:
|
|
4345
4417
|
return {
|
|
4346
4418
|
"workflow_available": max(
|
|
@@ -4442,20 +4514,12 @@ class Worker:
|
|
|
4442
4514
|
|
|
4443
4515
|
self._begin_run()
|
|
4444
4516
|
try:
|
|
4445
|
-
|
|
4446
|
-
await asyncio.wait_for(self._register(), timeout=timeout)
|
|
4447
|
-
finally:
|
|
4448
|
-
self._registration_done.set()
|
|
4517
|
+
await self._register_within(timeout)
|
|
4449
4518
|
|
|
4450
4519
|
startup_completed = True
|
|
4451
4520
|
if self._stop.is_set():
|
|
4452
4521
|
raise asyncio.CancelledError
|
|
4453
|
-
background_tasks.
|
|
4454
|
-
if self.workflows and self._query_tasks_supported:
|
|
4455
|
-
if self._can_clone_client_for_query_tasks():
|
|
4456
|
-
self._start_query_task_thread()
|
|
4457
|
-
else:
|
|
4458
|
-
background_tasks.append(asyncio.create_task(self._poll_query_tasks()))
|
|
4522
|
+
background_tasks.extend(self._start_run_until_background())
|
|
4459
4523
|
self._poller_tasks.update(background_tasks)
|
|
4460
4524
|
run_until_loop = asyncio.create_task(
|
|
4461
4525
|
self._run_until_loop(
|
|
@@ -4485,6 +4549,26 @@ class Worker:
|
|
|
4485
4549
|
raise primary_error.with_traceback(primary_traceback) from shutdown_error
|
|
4486
4550
|
raise
|
|
4487
4551
|
|
|
4552
|
+
async def _register_within(self, timeout: float) -> None:
|
|
4553
|
+
try:
|
|
4554
|
+
await asyncio.wait_for(self._register(), timeout=timeout)
|
|
4555
|
+
except Exception:
|
|
4556
|
+
self._status.failed("registration")
|
|
4557
|
+
raise
|
|
4558
|
+
finally:
|
|
4559
|
+
self._registration_done.set()
|
|
4560
|
+
|
|
4561
|
+
def _start_run_until_background(self) -> list[asyncio.Task[None]]:
|
|
4562
|
+
query_tasks = bool(self.workflows and self._query_tasks_supported)
|
|
4563
|
+
query_thread = query_tasks and self._can_clone_client_for_query_tasks()
|
|
4564
|
+
self._status.roles_started(("heartbeat", "query_poll") if query_tasks else ("heartbeat",))
|
|
4565
|
+
tasks = [self._start_status_role("heartbeat", self._heartbeat_loop())]
|
|
4566
|
+
if query_thread:
|
|
4567
|
+
self._start_query_task_thread()
|
|
4568
|
+
elif query_tasks:
|
|
4569
|
+
tasks.append(self._start_status_role("query_poll", self._poll_query_tasks()))
|
|
4570
|
+
return tasks
|
|
4571
|
+
|
|
4488
4572
|
async def _run_until_loop(
|
|
4489
4573
|
self,
|
|
4490
4574
|
*,
|
|
@@ -4644,10 +4728,113 @@ class Worker:
|
|
|
4644
4728
|
if self._stop.is_set():
|
|
4645
4729
|
raise RuntimeError("worker cannot run after stop() has been called")
|
|
4646
4730
|
self._run_started = True
|
|
4731
|
+
self._status.run_started()
|
|
4732
|
+
self._status_tick_task = asyncio.get_running_loop().create_task(self._status_tick_loop())
|
|
4647
4733
|
payload_observers = getattr(self.client, "_workflow_payload_observers", None)
|
|
4648
4734
|
if isinstance(payload_observers, dict):
|
|
4649
4735
|
payload_observers[self.worker_id] = self._renew_workflow_payload_lease
|
|
4650
4736
|
|
|
4737
|
+
def status(
|
|
4738
|
+
self,
|
|
4739
|
+
*,
|
|
4740
|
+
event_loop_stall_seconds: float = DEFAULT_EVENT_LOOP_STALL_SECONDS,
|
|
4741
|
+
request_overdue_seconds: float | None = None,
|
|
4742
|
+
request_stall_seconds: float | None = None,
|
|
4743
|
+
) -> WorkerStatus:
|
|
4744
|
+
"""Return a read-only readiness and liveness snapshot of this worker.
|
|
4745
|
+
|
|
4746
|
+
The snapshot performs no network I/O, claims no tasks, renews no leases
|
|
4747
|
+
and does not wait on the worker's event loop, so a health endpoint may
|
|
4748
|
+
call it from any thread, including while the event loop is stalled.
|
|
4749
|
+
|
|
4750
|
+
Readiness follows this worker's own registration: it becomes ready
|
|
4751
|
+
once the Server acknowledges the registration, while every configured
|
|
4752
|
+
poller is running, the latest Server exchange of every role succeeded
|
|
4753
|
+
and no outstanding request is older than ``request_overdue_seconds``.
|
|
4754
|
+
A transient Server or network failure makes the worker unready; the
|
|
4755
|
+
next successful exchange of that role restores readiness. Liveness
|
|
4756
|
+
reports local progress: the event loop advanced within
|
|
4757
|
+
``event_loop_stall_seconds``, no configured poller has exited, and no
|
|
4758
|
+
outstanding Server request is older than ``request_stall_seconds``.
|
|
4759
|
+
Idle long polls, Server outages within the transport retry budget and
|
|
4760
|
+
long-running activity handlers keep the worker live. Draining workers
|
|
4761
|
+
stay live but are not ready; stopped and failed workers are neither.
|
|
4762
|
+
|
|
4763
|
+
Args:
|
|
4764
|
+
event_loop_stall_seconds: Maximum age of the event loop's progress
|
|
4765
|
+
tick before the worker is reported not live.
|
|
4766
|
+
request_overdue_seconds: Maximum age of one outstanding poll or
|
|
4767
|
+
heartbeat before the worker is reported not ready. Defaults to
|
|
4768
|
+
one HTTP attempt deadline: the larger of the client request
|
|
4769
|
+
timeout and the poll's absolute HTTP deadline.
|
|
4770
|
+
request_stall_seconds: Maximum age of one outstanding poll or
|
|
4771
|
+
heartbeat before the worker is reported not live. Defaults to
|
|
4772
|
+
twice the client's complete transport retry budget for one
|
|
4773
|
+
such request.
|
|
4774
|
+
|
|
4775
|
+
Returns:
|
|
4776
|
+
A :class:`~durable_workflow.WorkerStatus` snapshot.
|
|
4777
|
+
"""
|
|
4778
|
+
if not _positive_seconds(event_loop_stall_seconds):
|
|
4779
|
+
raise ValueError("event_loop_stall_seconds must be a positive finite number")
|
|
4780
|
+
if request_overdue_seconds is None:
|
|
4781
|
+
request_overdue_seconds = self._request_attempt_deadline_seconds()
|
|
4782
|
+
elif not _positive_seconds(request_overdue_seconds):
|
|
4783
|
+
raise ValueError("request_overdue_seconds must be a positive finite number")
|
|
4784
|
+
if request_stall_seconds is None:
|
|
4785
|
+
request_stall_seconds = self._default_request_stall_seconds()
|
|
4786
|
+
elif not _positive_seconds(request_stall_seconds):
|
|
4787
|
+
raise ValueError("request_stall_seconds must be a positive finite number")
|
|
4788
|
+
namespace = self._registration_namespace if self._registration_worker_id is not None else getattr(
|
|
4789
|
+
self.client, "namespace", None
|
|
4790
|
+
)
|
|
4791
|
+
return self._status.snapshot(
|
|
4792
|
+
worker_id=self.worker_id,
|
|
4793
|
+
namespace=namespace if isinstance(namespace, str) else None,
|
|
4794
|
+
task_queue=self.task_queue,
|
|
4795
|
+
event_loop_stall_seconds=float(event_loop_stall_seconds),
|
|
4796
|
+
request_overdue_seconds=float(request_overdue_seconds),
|
|
4797
|
+
request_stall_seconds=float(request_stall_seconds),
|
|
4798
|
+
)
|
|
4799
|
+
|
|
4800
|
+
def _request_attempt_deadline_seconds(self) -> float:
|
|
4801
|
+
client_timeout = getattr(self.client, "timeout", None)
|
|
4802
|
+
request_timeout = (
|
|
4803
|
+
float(client_timeout)
|
|
4804
|
+
if isinstance(client_timeout, (int, float)) and _positive_seconds(client_timeout)
|
|
4805
|
+
else 60.0
|
|
4806
|
+
)
|
|
4807
|
+
poll_deadline = _worker_poll_http_timeout(self._poll_timeout) or 0.0
|
|
4808
|
+
return max(request_timeout, poll_deadline)
|
|
4809
|
+
|
|
4810
|
+
def _default_request_stall_seconds(self) -> float:
|
|
4811
|
+
# One logical request may retry transport failures; only a request
|
|
4812
|
+
# outliving twice that complete budget indicates a local stall.
|
|
4813
|
+
policy = getattr(self.client, "retry_policy", None)
|
|
4814
|
+
attempts = getattr(policy, "max_attempts", 3)
|
|
4815
|
+
backoff = getattr(policy, "max_backoff_seconds", 5.0)
|
|
4816
|
+
attempts = attempts if isinstance(attempts, int) and attempts >= 1 else 3
|
|
4817
|
+
backoff = float(backoff) if isinstance(backoff, (int, float)) and _positive_seconds(backoff) else 5.0
|
|
4818
|
+
budget = attempts * self._request_attempt_deadline_seconds() + (attempts - 1) * backoff
|
|
4819
|
+
return 2.0 * budget
|
|
4820
|
+
|
|
4821
|
+
async def _status_tick_loop(self) -> None:
|
|
4822
|
+
while True:
|
|
4823
|
+
self._status.loop_tick()
|
|
4824
|
+
await asyncio.sleep(EVENT_LOOP_TICK_SECONDS)
|
|
4825
|
+
|
|
4826
|
+
def _start_status_role(self, role: str, loop: Coroutine[Any, Any, None]) -> asyncio.Task[None]:
|
|
4827
|
+
task = asyncio.create_task(loop)
|
|
4828
|
+
task.add_done_callback(lambda _: self._status.role_exited(role))
|
|
4829
|
+
return task
|
|
4830
|
+
|
|
4831
|
+
def _record_poll_status(self, role: str, error: Exception, *, answered: bool) -> None:
|
|
4832
|
+
# Capacity backpressure is a Server answer for an admitted worker.
|
|
4833
|
+
if answered:
|
|
4834
|
+
self._status.request_succeeded(role)
|
|
4835
|
+
else:
|
|
4836
|
+
self._status.request_failed(role, error)
|
|
4837
|
+
|
|
4651
4838
|
def worker_session(self, options: WorkerSessionOptions) -> WorkerSession:
|
|
4652
4839
|
"""Return a typed lease handle held by this worker until shutdown."""
|
|
4653
4840
|
existing = self._worker_sessions.get(options.session_id)
|
|
@@ -4679,11 +4866,28 @@ class Worker:
|
|
|
4679
4866
|
stays within the original shutdown deadline. Unfenced deregistration
|
|
4680
4867
|
makes one attempt and does not retry an uncertain outcome.
|
|
4681
4868
|
"""
|
|
4869
|
+
self._status.draining()
|
|
4682
4870
|
self._stop.set()
|
|
4683
4871
|
if self._shutdown_task is None:
|
|
4684
|
-
self._shutdown_task = asyncio.create_task(self.
|
|
4872
|
+
self._shutdown_task = asyncio.create_task(self._shutdown_and_record_status())
|
|
4685
4873
|
await asyncio.shield(self._shutdown_task)
|
|
4686
4874
|
|
|
4875
|
+
async def _shutdown_and_record_status(self) -> None:
|
|
4876
|
+
try:
|
|
4877
|
+
await self._shutdown()
|
|
4878
|
+
except BaseException:
|
|
4879
|
+
self._status.failed("shutdown")
|
|
4880
|
+
raise
|
|
4881
|
+
else:
|
|
4882
|
+
self._status.stopped()
|
|
4883
|
+
finally:
|
|
4884
|
+
self._cancel_status_tick()
|
|
4885
|
+
|
|
4886
|
+
def _cancel_status_tick(self) -> None:
|
|
4887
|
+
tick = self._status_tick_task
|
|
4888
|
+
if tick is not None and not tick.done():
|
|
4889
|
+
tick.cancel()
|
|
4890
|
+
|
|
4687
4891
|
async def _shutdown(self) -> None:
|
|
4688
4892
|
if self._run_started and not self._registration_done.is_set():
|
|
4689
4893
|
await self._registration_done.wait()
|