Skip to content

Commit d75d795

Browse files
committed
test(startup): add startup_ordering test for gate sequencing invariants
1 parent b88c8ee commit d75d795

1 file changed

Lines changed: 144 additions & 0 deletions

File tree

nodedb/tests/startup_ordering.rs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//! Integration test: StartupSequencer phase ordering.
2+
//!
3+
//! Verifies that:
4+
//! - Phases advance only when all gates for that phase have fired.
5+
//! - Registering gates out of order is accepted; the phase each gate belongs to
6+
//! is determined by the `StartupPhase` passed to `register_gate`.
7+
//! - Firing a later-phase gate before an earlier-phase gate does not advance
8+
//! past the earlier phase until all earlier gates also fire.
9+
//! - `GatewayEnable` is only reached after all prior phases complete.
10+
11+
use std::sync::Arc;
12+
use std::time::Duration;
13+
14+
use nodedb::control::startup::{StartupGate, StartupPhase, StartupSequencer};
15+
16+
/// Assert that the gate reaches at least `expected`, timing out after 500 ms.
17+
///
18+
/// The current phase may have advanced beyond `expected` by the time we
19+
/// observe it, so we only assert `current_phase() >= expected`.
20+
async fn assert_phase_reaches(gate: &Arc<StartupGate>, expected: StartupPhase) {
21+
tokio::time::timeout(Duration::from_millis(500), gate.await_phase(expected))
22+
.await
23+
.expect("timed out waiting for phase")
24+
.expect("sequencer failed while waiting for phase");
25+
assert!(
26+
gate.current_phase() >= expected,
27+
"expected phase >= {expected:?}, got {:?}",
28+
gate.current_phase()
29+
);
30+
}
31+
32+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
33+
async fn phases_advance_in_order_when_gates_fire() {
34+
let (seq, gate) = StartupSequencer::new();
35+
36+
// Register one gate per phase (skipping Boot which is the initial phase).
37+
let wal_gate = seq.register_gate(StartupPhase::WalRecovery, "wal");
38+
let catalog_gate = seq.register_gate(StartupPhase::ClusterCatalogOpen, "catalog");
39+
let raft_gate = seq.register_gate(StartupPhase::RaftMetadataReplay, "raft");
40+
let schema_gate = seq.register_gate(StartupPhase::SchemaCacheWarmup, "schema");
41+
let sanity_gate = seq.register_gate(StartupPhase::CatalogSanityCheck, "sanity");
42+
let data_gate = seq.register_gate(StartupPhase::DataGroupsReplay, "data");
43+
let transport_gate = seq.register_gate(StartupPhase::TransportBind, "transport");
44+
let peers_gate = seq.register_gate(StartupPhase::WarmPeers, "peers");
45+
let health_gate = seq.register_gate(StartupPhase::HealthLoopStart, "health");
46+
let gw_gate = seq.register_gate(StartupPhase::GatewayEnable, "gateway");
47+
48+
// Initial phase is Boot.
49+
assert_eq!(gate.current_phase(), StartupPhase::Boot);
50+
51+
// Fire gates in strict phase order.
52+
wal_gate.fire();
53+
assert_phase_reaches(&gate, StartupPhase::WalRecovery).await;
54+
55+
catalog_gate.fire();
56+
assert_phase_reaches(&gate, StartupPhase::ClusterCatalogOpen).await;
57+
58+
raft_gate.fire();
59+
assert_phase_reaches(&gate, StartupPhase::RaftMetadataReplay).await;
60+
61+
schema_gate.fire();
62+
assert_phase_reaches(&gate, StartupPhase::SchemaCacheWarmup).await;
63+
64+
sanity_gate.fire();
65+
assert_phase_reaches(&gate, StartupPhase::CatalogSanityCheck).await;
66+
67+
data_gate.fire();
68+
assert_phase_reaches(&gate, StartupPhase::DataGroupsReplay).await;
69+
70+
transport_gate.fire();
71+
assert_phase_reaches(&gate, StartupPhase::TransportBind).await;
72+
73+
peers_gate.fire();
74+
assert_phase_reaches(&gate, StartupPhase::WarmPeers).await;
75+
76+
health_gate.fire();
77+
assert_phase_reaches(&gate, StartupPhase::HealthLoopStart).await;
78+
79+
gw_gate.fire();
80+
assert_phase_reaches(&gate, StartupPhase::GatewayEnable).await;
81+
}
82+
83+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
84+
async fn later_phase_gate_fires_first_does_not_advance_past_earlier_phase() {
85+
let (seq, gate) = StartupSequencer::new();
86+
87+
let wal_gate = seq.register_gate(StartupPhase::WalRecovery, "wal");
88+
let gw_gate = seq.register_gate(StartupPhase::GatewayEnable, "gateway");
89+
90+
// Fire GatewayEnable first — phase must not advance past Boot until WalRecovery fires.
91+
gw_gate.fire();
92+
93+
// Wait a bit and confirm we're still at Boot.
94+
tokio::time::sleep(Duration::from_millis(20)).await;
95+
assert_eq!(
96+
gate.current_phase(),
97+
StartupPhase::Boot,
98+
"phase advanced past Boot even though WalRecovery gate has not fired"
99+
);
100+
101+
// Now fire WalRecovery — phase should advance all the way to GatewayEnable
102+
// since the GatewayEnable gate already fired.
103+
wal_gate.fire();
104+
assert_phase_reaches(&gate, StartupPhase::GatewayEnable).await;
105+
}
106+
107+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
108+
async fn multiple_gates_for_same_phase_all_must_fire() {
109+
let (seq, gate) = StartupSequencer::new();
110+
111+
// Register two gates for the same phase.
112+
let wal_gate_a = seq.register_gate(StartupPhase::WalRecovery, "wal-primary");
113+
let wal_gate_b = seq.register_gate(StartupPhase::WalRecovery, "wal-secondary");
114+
115+
// Fire only the first — phase must not advance yet.
116+
wal_gate_a.fire();
117+
tokio::time::sleep(Duration::from_millis(20)).await;
118+
assert_eq!(
119+
gate.current_phase(),
120+
StartupPhase::Boot,
121+
"phase advanced after only one of two WalRecovery gates fired"
122+
);
123+
124+
// Fire the second — now the phase should advance.
125+
wal_gate_b.fire();
126+
assert_phase_reaches(&gate, StartupPhase::WalRecovery).await;
127+
}
128+
129+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
130+
async fn gate_fire_is_idempotent() {
131+
let (seq, gate) = StartupSequencer::new();
132+
133+
let wal_gate = seq.register_gate(StartupPhase::WalRecovery, "wal");
134+
135+
// Firing the same gate multiple times must not cause errors or double-advance.
136+
wal_gate.fire();
137+
wal_gate.fire();
138+
wal_gate.fire();
139+
140+
// Firing three times must succeed and advance the phase at least to WalRecovery.
141+
// With no later gates registered, the sequencer may advance all the way to
142+
// GatewayEnable — that is expected and correct.
143+
assert_phase_reaches(&gate, StartupPhase::WalRecovery).await;
144+
}

0 commit comments

Comments
 (0)