fix(maintenance): continue confirmed drain before stopping dependencies

This commit is contained in:
2026-10-13 08:49:23 +08:00
parent 110a4952a8
commit b575462c95
3 changed files with 82 additions and 4 deletions
+6 -2
View File
@@ -384,8 +384,12 @@ and hash-checks existing source/case bytes with a 64-KiB buffer. It creates no
storage directory or lock and changes no metadata, grade, session or grant.
Its output is private and does not itself prove that writers have stopped.
After the confirmed drain, stop the selected database and broker and verify all
their processes have exited. `python3.13 -m scripts.release.backup capture
After collecting the inventory, run `lifecycle --action stop`. For an already
quiescent node it reuses only this boot's successful selected owner-derived drain
receipt, rechecks every application cgroup and the Root task cleanup, then stops
the selected database and broker. It does not query owners already stopped or
adopt a stale, failed or unproven cut. Verify all their processes have exited.
`python3.13 -m scripts.release.backup capture
--backup-id bk-<32-lowercase-hex> --references PRIVATE_INVENTORY
--references-sha256 SHA` launches one fixed Root backup operator. It requires the
same selected release/configuration, this boot's successful owner-derived cut,
+38 -2
View File
@@ -430,12 +430,46 @@ class Maintenance:
final = self.snapshot()
if not projection_empty(final):
raise DeliveryError("work changed during final stop; quiescence unconfirmed")
self.record["drain_projection"] = final
self.change("coordinator", "stop")
self.change("sandbox-factory", "stop")
self.root_stopped()
availability.write(self.selected, "quiescent")
self.save("quiescent")
def reuse_drained(self, previous: dict | None) -> bool:
"""Continue a confirmed drain without querying owners already stopped."""
gate = availability.read()
if gate is None or gate.state != "quiescent":
return False
if previous is None:
raise DeliveryError("quiescent node has no confirmed drain receipt")
validate_record(previous)
if (
gate.release_id != self.selected["release_id"]
or gate.configuration_id != self.selected["configuration_id"]
or any(
previous[key] != self.record[key]
for key in ("source_sha", "configuration_id", "boot_id")
)
or previous["action"] not in {"drain", "stop"}
or previous["phase"] != "complete"
or previous["success"] is not True
or previous["root_cleanup_confirmed"] is not True
or any(event["confirmed"] is not True for event in previous["events"])
or "drain_projection" not in previous
or not projection_empty(previous["drain_projection"])
):
raise DeliveryError("prior drain is not this boot's selected owner-confirmed cut")
for name in APPS:
values = self.unit(name)
if values["ActiveState"] != "inactive" or values["MainPID"] != "0":
raise DeliveryError("prior drain has a running application service")
self.empty_cgroup(name)
self.record["drain_projection"] = previous["drain_projection"]
self.root_stopped()
return True
def start(self, *, resume: bool) -> None:
if not resume:
for name in APPS:
@@ -508,14 +542,16 @@ def run(action: str) -> dict:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
# Refuse unknown history before publishing availability, including
# resume where every known service could already be running.
previous = None
if LAST.exists() or LAST.is_symlink():
preflight.protected(LAST)
configuration.private(LAST)
validate_record(read_json(LAST, 32768))
previous = validate_record(read_json(LAST, 32768))
maintenance = Maintenance(selected, paths, payload, action)
try:
if action in {"drain", "stop", "restart"}:
maintenance.drain()
if not maintenance.reuse_drained(previous):
maintenance.drain()
if action == "stop":
for name in ("rabbitmq", "postgres"):
maintenance.change(name, "stop")