nixfleet_reconciler/planner_gates/channel_edges.rs
1//! Channel-edges gate (new-shape).
2//!
3//! Predecessor channel's rollout must be terminal-for-ordering before
4//! the successor opens. In the new architecture, "terminal-for-ordering"
5//! is a direct read of `RolloutSummary.terminal_at` — no more inferring
6//! from `host_states.values().all(...)` + `terminal_at` retrofit. The
7//! `is_active_for_ordering()` heuristic that the v0.2.0 `c3ab9d75` fix
8//! patched goes away with the old gate in Phase 6g.
9
10use nixfleet_proto::RolloutId;
11
12use crate::planner_gates::GateBlock;
13use crate::planner_types::{ChannelId, FleetState, SignedManifestSet};
14
15pub fn check(
16 fleet_state: &FleetState,
17 manifests: &SignedManifestSet,
18 successor_channel: &ChannelId,
19) -> Option<GateBlock> {
20 let fleet = manifests.fleet();
21
22 for edge in fleet
23 .channel_edges
24 .iter()
25 .filter(|e| e.gated == *successor_channel)
26 {
27 let predecessor = &edge.gates;
28 let blocked = predecessor_active(fleet_state, manifests, predecessor);
29 if blocked {
30 return Some(GateBlock::ChannelEdges {
31 predecessor_channel: predecessor.clone(),
32 });
33 }
34 }
35 None
36}
37
38/// True if the predecessor channel has in-flight work that must finish
39/// before the successor advances.
40///
41/// Source-of-truth precedence:
42///
43/// 1. No verified manifest for the predecessor channel ⇒ no work declared,
44/// return false (gate passes).
45/// 2. Manifest present, no rollout row yet for the manifest's current
46/// target_ref ⇒ fresh-boot protection: the predecessor's rollout is
47/// about to open, block conservatively so the successor cannot race
48/// ahead.
49/// 3. Manifest present, rollout row exists ⇒ return `terminal_at.is_none()`
50/// — in-flight blocks, Terminal passes.
51///
52/// LOADBEARING: keyed by canonical `RolloutId::new(channel,
53/// channel_ref)` (RFC-0008 §6.3), NOT by channel. A channel-level
54/// key would conflate "no rollout yet on this channel" with "rollout
55/// for this target_ref terminal" — the `terminal_at.is_none()`
56/// filter on a channel-keyed map causes the fresh-boot protection to
57/// misfire as a permanent block after legitimate completion. Mirror
58/// of the planner's rollout-id-keyed OpenRollout predicate at
59/// planner.rs's section 1.
60fn predecessor_active(
61 fleet_state: &FleetState,
62 manifests: &SignedManifestSet,
63 predecessor: &ChannelId,
64) -> bool {
65 let Some(rollout_manifest) = manifests.rollouts.get(predecessor) else {
66 return false;
67 };
68 let rollout_id = RolloutId::new(predecessor, &rollout_manifest.inner().channel_ref);
69 let Some(summary) = fleet_state.rollouts.get(&rollout_id) else {
70 return true;
71 };
72 summary.terminal_at.is_none()
73}