Node health: don't quarantine the last node able to take work when others tripped with it #36
Loading…
Reference in a new issue
No description provided.
Delete branch "breaker-last-healthy-node"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What. The per-node circuit breaker is now judged across the whole fleet (
scheduler/health.rs::blocked_nodes, SPEC §7.1).The hold. It applies when the breakers of two or more able nodes have tripped and no able node is left. A node is able if it is connected, not draining and not below the disk floor; a closed breaker or one on probation leaves it able. In that case the node whose cool-off ends first is held on probation instead of being opened; the name breaks a tie. It takes one task at a time. A success closes its breaker. A failure restarts its cool-off, and the hold passes to whichever node's cool-off now ends first.
When nothing changes:
Details:
/api/nodesall use the same function, and the tie-break keeps the choice stable.Visibility:
GET /api/nodesshowshealth.held: true. While the held node has a task out,unschedulablereadson probation: one task at a time./ui/nodesshows aprobationbadge and "held: the last node able to take work".warn!when a hold starts,debug!when it moves to another node,info!when it is released. The state lives inAppState::breaker_held.load_nodesnow takes the liveNodeSnapshots, so/api/nodesjudges connected nodes the same way the matchmaker does.Why. Before this, a cause shared by every node could quarantine the whole fleet for
quarantine_secsand stop all CI. Examples are a rev whose checkout fails everywhere, or the git proxy being down. This was the open item in docs/TODO.md from the #8 review.The hold requires two tripped nodes. A lone node is still bounded by its cool-off, which is the throttle the breaker exists for in the 2026-09-27 black-hole case.
Discounting failures that other nodes share is not done. It needs a definition of "the same cause" that could hide a genuinely broken node, so it stays open in TODO.md. Also still open: the hold is per fleet, not per task, and a lone able node waits out its cool-off.
Tests.
Probingwhile it has a task out, and its success releases the hold.it::node_health::a_black_hole_node_is_quarantined, restored:/api/queuewhy_not.eliminated.alphasays "quarantined".it::node_health::a_shared_cause_holds_one_node_on_probation, new:cargo test --workspaceandcargo clippy --workspace --all-targets -- -D warningspass. I have not run the nix gate (nix build .#ci.*) locally because nix is not installed in the VM. No files were added, so no fileset changes are needed.Deploy notes.
/api/nodesgains one field,health.held.🤖 Generated with Claude Code
Automated review (reviewer subagent) of
ca587e5c97Verdict: request changes. The mechanics are sound, but two things block.
Gates:
cargo clippy --workspace --all-targets -j2 -- -D warningsis clean.cargo test --workspaceis not verified: the sandbox VM's disk went read-only mid-run because of host block-I/O errors. Mutation checks were argued from the code, not executed.Blocking
scheduler/health.rs:403-419, SPEC.md:945-959.quarantine_secs.open.len() >= 2(at least two able nodes tripped, so a shared cause is plausible); a lone node keeps the bounded cool-off. Update SPEC §7.1, the deploy notes, and the "a lone node is held" leg ofpeers_that_take_no_work_do_not_count(health.rs:856). Optionally pace held probes.tests/it/node_health.rs:198-222.alpha.next_assign(1s).is_none()runs after beta has finished a, b and c, so it is vacuous.avoided_nodesalone.blockedwould likely still pass.a_lone_node_is_held_on_probationbecomes whatever #1 settles on. Restorea_black_hole_node_is_quarantinedwith work queued that alpha has not failed (e.g. beta busy or too small). Assert that alpha gets nothing while that task waits, and that/api/queuewhy_not.eliminated.alphacontains "quarantined".Non-blocking
health.rs:430-445: the warning is deduped per node, not per hold, so with two nodes a shared cause warns on every failed probe, and a release is silent. Dedupe on "a hold exists",info!when it ends,debug!on a move.health.rs:~793: nothing tests the hold passing to the peer whose cool-off ends first, or the name tie-break. Add both.docs/forgejo-setup.md:209-214still says a tripped node "gets no new work". Mention the hold.Checked and fine
load_nodes,load_queueand/api/nodeseach mutate a fresh map;warn_heldis called only from matchmaking.health.heldis additive.Node health: never quarantine the last node able to take workto Node health: don't quarantine the last node able to take work when others tripped with itView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.