From 41d0e3d195d5c43af63315c50d3159df4645aac9 Mon Sep 17 00:00:00 2001 From: kronecker <2026692687@qq.com> Date: Wed, 14 Oct 2026 22:02:31 +0800 Subject: [PATCH] fix: close readiness before querying unavailable services --- backend/tests/test_delivery.py | 39 ++++++++++++++++++++++++++++++++++ scripts/release/node_health.py | 16 +++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_delivery.py b/backend/tests/test_delivery.py index 29337d9..61310d7 100644 --- a/backend/tests/test_delivery.py +++ b/backend/tests/test_delivery.py @@ -1766,6 +1766,45 @@ def test_readiness_rejects_wrong_or_excess_database_authority(field, value): node_health.validate_role("coordinator", observed) +@pytest.mark.parametrize( + "missing", [*services.PROFILES, "postgres", "rabbitmq", "sandbox-factory", "proxy"] +) +@pytest.mark.parametrize("probe", [False, True]) +def test_missing_service_closes_readiness_without_dependency_queries( + monkeypatch, tmp_path, missing, probe +): + selected = {"release_id": "sd-123456789abc", "configuration_id": "cfg-" + "1" * 24} + payload = tmp_path / "payload" + payload.mkdir() + (tmp_path / "manifest.json").write_text(json.dumps({"source_sha": "a" * 40})) + monkeypatch.setattr(node_health.operator_units, "guarded", lambda _: None) + monkeypatch.setattr(node_health, "binding", lambda: (selected, {}, payload)) + monkeypatch.setattr(journal, "check_ready", lambda: {}) + monkeypatch.setattr(proxy_install, "check_binding", lambda *args: {}) + monkeypatch.setattr(node_health.proxy_health, "observe", lambda _: {}) + monkeypatch.setattr(installer, "selection", lambda **kwargs: selected) + + def inspect(name, binding): + if name == missing: + raise policy.DeliveryError("selected service is not running") + return {"active": True} + + def unavailable(*args, **kwargs): + pytest.fail("missing required service must not start dependency queries") + + monkeypatch.setattr(node_health, "inspect_unit", inspect) + monkeypatch.setattr(node_health, "roles", unavailable) + monkeypatch.setattr(node_health, "http", unavailable) + monkeypatch.setattr(dependency_install, "installed_endpoints", unavailable) + monkeypatch.setattr(node_health.operator_units, "run", unavailable) + monkeypatch.setattr(node_health.availability, "read", unavailable) + observed = node_health.observe(probe=probe) + assert observed["ready_for_admission"] is False + assert observed["service_preconditions_met"] is False + assert observed["isolation_verified"] is False + assert observed["errors"] == [missing + "_unavailable"] + + def test_readiness_refuses_context_swap_or_claimed_isolation_certificate(): value = { "schema_version": 1, diff --git a/scripts/release/node_health.py b/scripts/release/node_health.py index 2d5a890..ba2ef22 100644 --- a/scripts/release/node_health.py +++ b/scripts/release/node_health.py @@ -494,11 +494,25 @@ def observe(*, probe: bool) -> dict: components["https_entry"] = proxy_health.observe(selected) except (OSError, ValueError, subprocess.SubprocessError): errors.append("https_entry_unavailable") - for name in [*services.PROFILES, "postgres", "rabbitmq", "sandbox-factory", "proxy"]: + services_ready = True + for name in [ + *services.PROFILES, + "postgres", + "rabbitmq", + "sandbox-factory", + "proxy", + ]: try: components[name] = inspect_unit(name, selected) except (OSError, ValueError): + services_ready = False errors.append(name + "_unavailable") + if not services_ready: + # Missing or invalid service profiles already close admission. Do not + # spend the outer helper deadline starting queries of those dependencies. + if install.selection(check_configuration=False) != selected: + errors.append("selected_binding_changed") + return result endpoints = dependency_install.installed_endpoints(selected) try: components["db_authority"] = asyncio.run(roles(endpoints))