fix(spurctld): keep the node record when its agent shuts down - #784
Open
pre wants to merge 1 commit into
Open
Conversation
spurd deregisters on SIGTERM, and the controller answered with remove_node(force = true), which deleted the node record. On a cluster with the WireGuard mesh, mesh_from_nodes builds the membership from that record, thus the k0s reconciliation pruned the peer of the node on every other node. The rebooted node could not register again, because the controllers are only reachable over the mesh it had just left. This is a deadlock: no part of it repairs itself, and the node stays out of Kubernetes and out of the batch cluster. A stopping agent is Down, not gone. DeregisterAgent now marks the node Down and keeps the record, its wg_pubkey and its mesh IP. The jobs are evicted exactly as before, because the Down transition evicts them. An admin hold keeps its reason and its lock. check_node_health recovers the node when its heartbeat returns. Removal stays an operator action: `spur node remove` and `spur k8s down`. Measured on a three-node cluster with k0s and the mesh: before the change, a reboot of a worker left sinfo at 2 nodes of 3, one peer on every node that stayed up, the Kubernetes node NotReady and spurd in a restart loop. After the change, two reboots in a row keep 3 nodes, 2 peers, the Kubernetes node Ready and no restart of spurd, and the node returns about 30 seconds after it comes up. The fault fires only when the rebooted node is not the Raft leader: the removal of the leader cannot commit while the leader goes down.
pre
requested review from
biluriuday,
sajmera-pensando,
sgopinath1,
shiv-tyagi and
yansun1996
as code owners
September 1, 2026 07:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
spurd deregisters on SIGTERM, and the controller answered with remove_node(force = true), which deleted the node record. On a cluster with the WireGuard mesh, mesh_from_nodes builds the membership from that record, thus the k0s reconciliation pruned the peer of the node on every other node. The rebooted node could not register again, because the controllers are only reachable over the mesh it had just left. This is a deadlock: no part of it repairs itself, and the node stays out of Kubernetes and out of the batch cluster.
A stopping agent is Down, not gone. DeregisterAgent now marks the node Down and keeps the record, its wg_pubkey and its mesh IP. The jobs are evicted exactly as before, because the Down transition evicts them. An admin hold keeps its reason and its lock. check_node_health recovers the node when its heartbeat returns. Removal stays an operator action:
spur node removeandspur k8s down.Measured on a three-node cluster with k0s and the mesh: before the change, a reboot of a worker left sinfo at 2 nodes of 3, one peer on every node that stayed up, the Kubernetes node NotReady and spurd in a restart loop. After the change, two reboots in a row keep 3 nodes, 2 peers, the Kubernetes node Ready and no restart of spurd, and the node returns about 30 seconds after it comes up.
The fault fires only when the rebooted node is not the Raft leader: the removal of the leader cannot commit while the leader goes down.
Motivation
A reboot is a normal event, so a kernel update or a power cut loses a node for good.
Symptom
On a cluster with the WireGuard mesh and k0s, rebooting a node takes it out of Kubernetes and out of the batch cluster, permanently. Nothing repairs itself, and no signal names the network:
sinfoNotReady, still after 5 minutesspurdon the rebooted nodeThe error on the rebooted node points at Raft, while the fault is in the mesh:
The fault fires only when the rebooted node is not the Raft leader: the removal of the leader cannot commit while the leader goes down, so rebooting the leader is survivable by accident. On a cluster of N nodes, N-1 of them break the cluster when they reboot.
Change
A stopping agent is Down, not gone.
DeregisterAgentnow marks the node Down and keeps the record, itswg_pubkeyand its mesh IP:Downtransition evicts them.check_node_healthrecovers the node when its heartbeat returns.spur node removeandspur k8s down.Test Plan
agent_shutdown_marks_node_down_and_keeps_itfails if the record, thewg_pubkeyor the automatic recovery is lost. 963 unit tests pass.Verified on a three-node cluster with k0s and the WireGuard mesh, the k0s control plane on node 2 and a worker on node 3:
node removed from clusteron the survivorsmarked DOWN, then recovered about 30 s latersinfoNotReadyReady, it never leftspurdrestartswg syncconfneededTwo reboots in a row, plus a batch job afterwards. A reboot of all three nodes at the same time also comes back complete.
Test Result
Verification so far, since the workflow runs need maintainer approval:
Local, on this branch alone (rebased on 360d44c):
cargo fmt --check --allcleancargo clippy -p spurctldno warningsagent_shutdown_marks_node_down_and_keeps_itOn a three-node cluster with the WireGuard mesh and k0s, control plane on
node 2 and a worker on node 3, with the patched
spurctldon all three:sinfoat 2 nodes of 3, one peer on eachnode that stayed up, the Kubernetes node
NotReadyandspurdat 37restarts. Reproduced twice.
Kubernetes node
Readythroughout andspurdat 0 restarts. The node isback in
sinfoabout 30 s after it comes up, with no command by hand, andthe journal shows
node marked DOWN (agent shutdown), record keptfollowedby
node recovered (heartbeat resumed).same time also comes back complete.
The E2E suite has not run yet. Could a maintainer approve the workflow runs?
Submission Checklist