fix(release): honor factory preparation startup deadline

This commit is contained in:
2026-10-16 13:38:49 +08:00
parent e159576340
commit 2cae75920d
3 changed files with 66 additions and 5 deletions
+38
View File
@@ -486,6 +486,44 @@ def test_startup_cannot_launch_services_when_recovery_admission_is_refused(monke
assert calls == []
@pytest.mark.parametrize(
("name", "ready_after", "succeeds"),
[("sandbox-factory", 131, True), ("sandbox-factory", 1811, False), ("api", 131, False)],
)
def test_startup_waits_for_declared_factory_preparation_without_extending_other_services(
monkeypatch, name, ready_after, succeeds
):
maintenance = object.__new__(lifecycle.Maintenance)
maintenance.selected = {"release_id": "sd-" + "a" * 12, "configuration_id": "cfg-" + "b" * 24}
maintenance.record = {"events": [], "root_cleanup_confirmed": True}
clock = [0.0]
confirmed = []
monkeypatch.setattr(lifecycle.time, "monotonic", lambda: clock[0])
monkeypatch.setattr(lifecycle.time, "sleep", lambda _: clock.__setitem__(0, clock[0] + 1))
monkeypatch.setattr(lifecycle.install, "selection", lambda **_: maintenance.selected)
monkeypatch.setattr(lifecycle.service_install, "systemctl", lambda *args: "")
monkeypatch.setattr(maintenance, "save", lambda phase: None)
monkeypatch.setattr(
maintenance,
"unit",
lambda _: {
"ActiveState": "active" if clock[0] >= ready_after else "activating",
"SubState": "running" if clock[0] >= ready_after else "start-pre",
"MainPID": "123" if clock[0] >= ready_after else "0",
},
)
monkeypatch.setattr(lifecycle.node_health, "inspect_unit", lambda *args: confirmed.append(args))
if succeeds:
maintenance.change(name, "start")
assert maintenance.record["events"][-1]["confirmed"] is True
assert confirmed == [(name, maintenance.selected)]
else:
with pytest.raises(policy.DeliveryError, match="bounded lifecycle operation unconfirmed"):
maintenance.change(name, "start")
assert maintenance.record["events"][-1]["confirmed"] is False
assert confirmed == []
def test_restoration_keeps_old_runtime_configuration_and_operations_inactive():
directory = Path("/var/lib/kf-onlinejudge/recovery/rr-" + "1" * 32)
for domain in restore.HISTORICAL:
+14 -3
View File
@@ -14,6 +14,7 @@ from .policy import DeliveryError, canonical, digest, fields, read_json, sha256
UNIT = "kf-sandbox-factory.service"
UNITS = Path("/etc/kf-judge/units")
START_TIMEOUT_SECONDS = 1800
def validate_receipt(value: dict) -> dict:
@@ -143,7 +144,7 @@ def render(receipt: dict) -> bytes:
"LimitFSIZE=64M",
"LimitNOFILE=256",
"LimitSTACK=16M",
"TimeoutStartSec=1800",
f"TimeoutStartSec={START_TIMEOUT_SECONDS}",
"RuntimeMaxSec=3700",
"TimeoutStopSec=40",
"KillMode=mixed",
@@ -164,7 +165,12 @@ def generate(output: Path) -> dict:
raise DeliveryError("Root factory composition requires trusted operator")
selected, original = service_install.current_receipt()
payload = install.RELEASES / selected["release_id"] / "payload"
for name in ("factory_service.py", "services.py", "service_profiles.py", "layout.py"):
for name in (
"factory_service.py",
"services.py",
"service_profiles.py",
"layout.py",
):
installed = payload / "delivery/scripts/release" / name
preflight.protected(installed)
if digest(installed) != digest(Path(__file__).parent / name):
@@ -211,7 +217,12 @@ def install_unit(source: Path, expected: str) -> dict:
):
raise DeliveryError("factory unit does not belong to selected configuration")
payload = install.RELEASES / selected["release_id"] / "payload"
for name in ("factory_service.py", "services.py", "service_profiles.py", "layout.py"):
for name in (
"factory_service.py",
"services.py",
"service_profiles.py",
"layout.py",
):
shipped = payload / "delivery/scripts/release" / name
preflight.protected(shipped)
if digest(shipped) != digest(Path(__file__).parent / name):
+14 -2
View File
@@ -30,7 +30,15 @@ from .policy import DeliveryError, canonical, digest, fields, read_json, sha256
STATE = Path("/var/lib/kf-onlinejudge/operations")
LAST = STATE / "maintenance.last.json"
APPS = ("proxy", "api", "outbox", "worker", "problem-import", "coordinator", "sandbox-factory")
APPS = (
"proxy",
"api",
"outbox",
"worker",
"problem-import",
"coordinator",
"sandbox-factory",
)
DEPENDENCIES = ("postgres", "rabbitmq")
START_ORDER = (
"sandbox-factory",
@@ -337,7 +345,11 @@ class Maintenance:
self.record["events"].append({"unit": name, "operation": operation, "confirmed": False})
self.save("dispatching_" + operation)
service_install.systemctl("--no-block", operation, "kf-" + name + ".service")
deadline = time.monotonic() + (130 if operation == "start" else 50)
# Root preparation validates the protected image registry before exec.
# Its declared unit timeout also applies here; the enclosing maintenance
# helper still enforces its independent overall runtime ceiling.
start_seconds = factory_service.START_TIMEOUT_SECONDS if name == "sandbox-factory" else 120
deadline = time.monotonic() + (start_seconds + 10 if operation == "start" else 50)
while True:
if install.selection(check_configuration=False) != self.selected:
raise DeliveryError("selected binding changed during maintenance")