nixfleet_reconciler/planner_gates/
host_edges.rs

1//! Host-edges gate. Per-host DAG within a single rollout:
2//! `Edge { gated: A, gates: B }` holds A's dispatch until B is
3//! ordering-eligible โ€” Converged (canonical "health-verified at
4//! target") OR Deferred (activation staged, live-switch pending
5//! operator reboot per RFC-0005 ยง3 terminal-for-ordering).
6//!
7//! LOADBEARING: Deferred counts as ordering-eligible. Without this,
8//! a single host that hit `DeferredPendingReboot` (framework upgrade
9//! touching dbus/systemd/kernel/init) would halt the cascade
10//! indefinitely on downstream host-edge dependencies. Deferred is
11//! "this host is done participating in the rollout step from an
12//! ordering standpoint"; actual health verification (probes, soak)
13//! runs once the operator reboots.
14
15use nixfleet_state_machine::HostState;
16
17use crate::planner_gates::GateBlock;
18use crate::planner_types::{FleetState, HostId, RolloutId, SignedManifestSet};
19
20pub fn check(
21    fleet_state: &FleetState,
22    manifests: &SignedManifestSet,
23    host: &HostId,
24    rollout_id: &RolloutId,
25) -> Option<GateBlock> {
26    let fleet = manifests.fleet();
27    let host_channel = fleet.hosts.get(host).map(|h| h.channel.as_str())?;
28
29    for edge in fleet.edges.iter().filter(|e| e.gated == *host) {
30        // Cross-channel guard: silently skip edges where the gating host
31        // is on a different channel โ€” that's `channel_edges`'s job, and
32        // looking up such a host in this rollout's host_states would
33        // always miss and block forever (the bug the old gate's
34        // cross-channel filter was added to prevent).
35        let same_channel = fleet
36            .hosts
37            .get(&edge.gates)
38            .map(|h| h.channel == host_channel)
39            .unwrap_or(false);
40        if !same_channel {
41            continue;
42        }
43
44        // Look up the gating host's state within THIS rollout. Absence
45        // means the gating host hasn't started yet โ†’ block.
46        let key = (rollout_id.clone(), edge.gates.clone());
47        let state = fleet_state.host_states.get(&key).map(|s| s.state);
48        let ordering_eligible = matches!(
49            state,
50            Some(HostState::Converged) | Some(HostState::Deferred)
51        );
52        if !ordering_eligible {
53            return Some(GateBlock::HostEdge {
54                gating_host: edge.gates.clone(),
55            });
56        }
57    }
58    None
59}