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}