diff --git a/pyproject.toml b/pyproject.toml index 3d5981f..9fb2b31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "durable-workflow" -version = "2.3.0" +version = "2.3.1" description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server" readme = "README.md" requires-python = ">=3.10" @@ -71,9 +71,9 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main" durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main" [tool.durable-workflow] -product-train = "2.3.0" -registry-version = "2.3.0" -supported-server-versions = "2.4.2" +product-train = "2.3.1" +registry-version = "2.3.1" +supported-server-versions = "2.4.8" worker-protocol-version = "1.19" control-plane-version = "2" durable-selection = true diff --git a/src/durable_workflow/retry_policy.py b/src/durable_workflow/retry_policy.py index de12b3f..9322c19 100644 --- a/src/durable_workflow/retry_policy.py +++ b/src/durable_workflow/retry_policy.py @@ -68,25 +68,26 @@ def _storage_refusal(exc: Exception) -> tuple[ServerError, str | None] | None: def _backend_unavailable_refusal(exc: Exception) -> tuple[bool, int | None]: if not isinstance(exc, httpx.HTTPStatusError) or exc.response.status_code != 503: return False, None - try: - body = exc.response.json() - except ValueError: - return False, None - if not isinstance(body, dict) or body.get("reason") != "backend_unavailable": - return False, None request = exc.request if request.method != "POST" or "X-Durable-Workflow-Protocol-Version" not in request.headers: - return True, None + return False, None operations = { "/api/worker/workflow-tasks/poll": "poll_workflow_task", "/api/worker/activity-tasks/poll": "poll_activity_task", "/api/worker/query-tasks/poll": "poll_query_task", + "/api/worker/update-validation-tasks/poll": "poll_update_validation_task", "/api/worker/register": "register_worker", "/api/worker/heartbeat": "heartbeat_worker", } operation = next((name for path, name in operations.items() if request.url.path.endswith(path)), None) if operation is None: - return True, None + return False, None + try: + body = exc.response.json() + except ValueError: + return False, None + if not isinstance(body, dict) or body.get("reason") != "backend_unavailable": + return False, None try: submitted = json.loads(request.content) except ValueError: diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py index a6c826c..846722b 100644 --- a/tests/test_release_metadata.py +++ b/tests/test_release_metadata.py @@ -44,7 +44,7 @@ def test_worker_release_identity_matches_supported_server_and_protocol() -> None assert check_release_metadata.VERSION_PATTERN.fullmatch(project["version"]) is not None assert release["product-train"] == project["version"] assert release["registry-version"] == project["version"].replace("-rc.", "rc") - assert release["supported-server-versions"] == "2.4.2" + assert check_release_metadata.VERSION_PATTERN.fullmatch(release["supported-server-versions"]) is not None assert release["worker-protocol-version"] == PROTOCOL_VERSION == "1.19" assert release["durable-selection"] is True assert release["durable-selection-minimum-worker-protocol-version"] == "1.19" diff --git a/tests/test_storage_admission.py b/tests/test_storage_admission.py index c861cad..fb4e8bb 100644 --- a/tests/test_storage_admission.py +++ b/tests/test_storage_admission.py @@ -249,6 +249,20 @@ def handler(request: httpx.Request) -> httpx.Response: assert calls == 2 +async def test_control_backend_error_keeps_finite_transport_retries(retry_sleeps: list[float]) -> None: + calls = 0 + + def handler(request: httpx.Request) -> httpx.Response: + nonlocal calls + calls += 1 + return httpx.Response(503, json={"reason": "backend_unavailable", "retryable": True}) + + async with client_for(handler) as client: + with worker_scope(), pytest.raises(ServerError): + await client._request("POST", "/workflows", json={}) + assert calls == 2 + + async def test_shutdown_interrupts_backend_outage_without_new_poll(monkeypatch: pytest.MonkeyPatch) -> None: calls = 0 stopped = False