1use super::door::require_dev_node;
19use super::{
20 CandidateBytes, DevReply, DevRequest, DevStatusEvent, GenerationReport, NodeMode, ReloadStage,
21 StageRefusal,
22};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum StartFailure {
28 Load(String),
30 Tree(String),
32}
33
34pub trait NodeControl {
38 fn stop_component(&mut self) -> Result<(), String>;
45 fn stage(&mut self, candidate: &CandidateBytes) -> Result<(), String>;
53 fn start_component(&mut self) -> Result<(), StartFailure>;
60 fn witness_mailbox(&mut self) -> Result<(), String>;
66 fn witness_content(&mut self) -> Result<(), String>;
72 fn report(
79 &mut self,
80 build_generation: u64,
81 content_digest: [u8; 32],
82 ) -> Result<GenerationReport, String>;
83}
84
85pub struct BarrierEngine {
87 mode: NodeMode,
88 last_good: CandidateBytes,
91 newest_seen: u64,
95 in_flight: bool,
96}
97
98impl BarrierEngine {
99 #[must_use]
101 pub fn new(mode: NodeMode, boot: CandidateBytes) -> Self {
102 Self {
103 mode,
104 last_good: boot,
105 newest_seen: 0,
106 in_flight: false,
107 }
108 }
109
110 pub fn handle(
113 &mut self,
114 request: DevRequest,
115 node: &mut dyn NodeControl,
116 status: &mut dyn FnMut(DevStatusEvent),
117 ) -> DevReply {
118 let DevRequest::StageAndActivate(candidate) = request;
119 if let Err(refusal) = require_dev_node(self.mode) {
120 return DevReply::Refused(refusal);
121 }
122 if self.in_flight {
123 return DevReply::Refused(StageRefusal::ActivationInFlight);
124 }
125 if candidate.build_generation <= self.newest_seen {
126 return DevReply::Refused(StageRefusal::StaleGeneration {
127 offered: candidate.build_generation,
128 newest_seen: self.newest_seen,
129 });
130 }
131 self.newest_seen = candidate.build_generation;
132 self.in_flight = true;
133 let reply = self.activate(&candidate, node, status);
134 self.in_flight = false;
135 reply
136 }
137
138 fn activate(
139 &mut self,
140 candidate: &CandidateBytes,
141 node: &mut dyn NodeControl,
142 status: &mut dyn FnMut(DevStatusEvent),
143 ) -> DevReply {
144 let generation = candidate.build_generation;
145 let mut stage = |at: ReloadStage| {
146 status(DevStatusEvent::Reloading {
147 build_generation: generation,
148 stage: at,
149 });
150 };
151
152 stage(ReloadStage::Drain);
153 stage(ReloadStage::StopOld);
154 if let Err(detail) = node.stop_component() {
155 return self.roll_back(ReloadStage::StopOld, detail, node, status);
156 }
157 stage(ReloadStage::PurgeOld);
158 if let Err(detail) = node.stage(candidate) {
159 return self.roll_back(ReloadStage::PurgeOld, detail, node, status);
160 }
161 stage(ReloadStage::StartFresh);
162 match node.start_component() {
163 Ok(()) => {}
164 Err(StartFailure::Load(detail)) => {
165 return self.roll_back(ReloadStage::LoadCandidate, detail, node, status);
166 }
167 Err(StartFailure::Tree(detail)) => {
168 return self.roll_back(ReloadStage::StartFresh, detail, node, status);
169 }
170 }
171 stage(ReloadStage::LivenessMailbox);
172 if let Err(detail) = node.witness_mailbox() {
173 return self.roll_back_live(ReloadStage::LivenessMailbox, detail, node, status);
174 }
175 stage(ReloadStage::LivenessContent);
176 if let Err(detail) = node.witness_content() {
177 return self.roll_back_live(ReloadStage::LivenessContent, detail, node, status);
178 }
179
180 self.last_good = candidate.clone();
181 match node.report(generation, candidate.content_digest) {
182 Ok(report) => {
183 status(DevStatusEvent::RunningCurrent(report.clone()));
184 DevReply::Activated(report)
185 }
186 Err(detail) => node_failed(ReloadStage::Report, detail, status),
187 }
188 }
189
190 fn roll_back_live(
193 &mut self,
194 failed: ReloadStage,
195 detail: String,
196 node: &mut dyn NodeControl,
197 status: &mut dyn FnMut(DevStatusEvent),
198 ) -> DevReply {
199 if let Err(stop_detail) = node.stop_component() {
200 return node_failed(
201 failed,
202 format!(
203 "{detail}; stopping the unproven candidate tree also failed: {stop_detail}"
204 ),
205 status,
206 );
207 }
208 self.roll_back(failed, detail, node, status)
209 }
210
211 fn roll_back(
215 &mut self,
216 failed: ReloadStage,
217 detail: String,
218 node: &mut dyn NodeControl,
219 status: &mut dyn FnMut(DevStatusEvent),
220 ) -> DevReply {
221 let restore = self.restore_last_good(node);
222 match restore {
223 Ok(()) => {
224 let serving = match node.report(
225 self.last_good.build_generation,
226 self.last_good.content_digest,
227 ) {
228 Ok(serving) => serving,
229 Err(report_detail) => {
230 return node_failed(
231 failed,
232 format!("{detail}; last-good witnesses unreadable: {report_detail}"),
233 status,
234 );
235 }
236 };
237 status(DevStatusEvent::ReloadFailed {
238 failed_generation: self.newest_seen,
239 failed,
240 detail: detail.clone(),
241 serving: serving.clone(),
242 });
243 DevReply::RolledBack {
244 failed,
245 detail,
246 serving,
247 }
248 }
249 Err(restore_detail) => node_failed(
250 failed,
251 format!("{detail}; restoring last good failed: {restore_detail}"),
252 status,
253 ),
254 }
255 }
256
257 fn restore_last_good(&mut self, node: &mut dyn NodeControl) -> Result<(), String> {
258 node.stage(&self.last_good)
259 .map_err(|error| format!("restore stage: {error}"))?;
260 node.start_component().map_err(|error| match error {
261 StartFailure::Load(detail) => format!("restore load: {detail}"),
262 StartFailure::Tree(detail) => format!("restore start: {detail}"),
263 })?;
264 node.witness_mailbox()
265 .map_err(|error| format!("restore mailbox witness: {error}"))?;
266 node.witness_content()
267 .map_err(|error| format!("restore content witness: {error}"))
268 }
269}
270
271fn node_failed(
272 failed: ReloadStage,
273 detail: String,
274 status: &mut dyn FnMut(DevStatusEvent),
275) -> DevReply {
276 status(DevStatusEvent::NodeFailed {
277 detail: detail.clone(),
278 });
279 DevReply::NodeFailed { failed, detail }
280}
281
282#[cfg(test)]
283mod tests {
284 #![allow(
285 clippy::expect_used,
286 clippy::panic,
287 clippy::struct_excessive_bools,
288 clippy::cast_possible_truncation
289 )]
290
291 use super::{BarrierEngine, NodeControl, StartFailure};
292 use crate::dev::{
293 CandidateBytes, DevReply, DevRequest, DevStatusEvent, GenerationReport, NodeMode,
294 ReloadStage, StageRefusal,
295 };
296
297 #[derive(Default)]
300 struct FakeNode {
301 calls: Vec<String>,
302 fail_stop: bool,
303 fail_stage: bool,
304 fail_start: Option<StartFailure>,
305 fail_mailbox: bool,
306 fail_content: bool,
307 failures_left: usize,
309 }
310
311 impl FakeNode {
312 fn failing(field: fn(&mut FakeNode), failures: usize) -> Self {
313 let mut node = Self {
314 failures_left: failures,
315 ..Self::default()
316 };
317 field(&mut node);
318 node
319 }
320
321 fn consume_failure(&mut self) -> bool {
322 if self.failures_left > 0 {
323 self.failures_left -= 1;
324 true
325 } else {
326 false
327 }
328 }
329 }
330
331 impl NodeControl for FakeNode {
332 fn stop_component(&mut self) -> Result<(), String> {
333 self.calls.push("stop".to_owned());
334 if self.fail_stop && self.consume_failure() {
335 return Err("stop refused".to_owned());
336 }
337 Ok(())
338 }
339 fn stage(&mut self, candidate: &CandidateBytes) -> Result<(), String> {
340 self.calls
341 .push(format!("stage:{}", candidate.build_generation));
342 if self.fail_stage && self.consume_failure() {
343 return Err("purge refused: still referenced".to_owned());
344 }
345 Ok(())
346 }
347 fn start_component(&mut self) -> Result<(), StartFailure> {
348 self.calls.push("start".to_owned());
349 if let Some(failure) = self.fail_start.clone()
350 && self.consume_failure()
351 {
352 return Err(failure);
353 }
354 Ok(())
355 }
356 fn witness_mailbox(&mut self) -> Result<(), String> {
357 self.calls.push("mailbox".to_owned());
358 if self.fail_mailbox && self.consume_failure() {
359 return Err("mailbox witness failed".to_owned());
360 }
361 Ok(())
362 }
363 fn witness_content(&mut self) -> Result<(), String> {
364 self.calls.push("content".to_owned());
365 if self.fail_content && self.consume_failure() {
366 return Err("content witness failed".to_owned());
367 }
368 Ok(())
369 }
370 fn report(
371 &mut self,
372 build_generation: u64,
373 content_digest: [u8; 32],
374 ) -> Result<GenerationReport, String> {
375 Ok(GenerationReport {
376 build_generation,
377 content_digest,
378 component_incarnation: 1,
379 module_generation: 1,
380 })
381 }
382 }
383
384 fn candidate(generation: u64) -> CandidateBytes {
385 CandidateBytes {
386 build_generation: generation,
387 content_digest: [generation as u8; 32],
388 component_beam: vec![generation as u8],
389 ffi_beam: vec![generation as u8, 0xff],
390 }
391 }
392
393 fn engine(mode: NodeMode) -> BarrierEngine {
394 BarrierEngine::new(mode, candidate(0))
395 }
396
397 fn drive(
398 engine: &mut BarrierEngine,
399 node: &mut FakeNode,
400 generation: u64,
401 ) -> (DevReply, Vec<DevStatusEvent>) {
402 let mut events = Vec::new();
403 let reply = engine.handle(
404 DevRequest::StageAndActivate(candidate(generation)),
405 node,
406 &mut |event| events.push(event),
407 );
408 (reply, events)
409 }
410
411 #[test]
414 fn production_engine_refuses_with_zero_node_calls() {
415 let mut node = FakeNode::default();
416 let (reply, events) = drive(&mut engine(NodeMode::Production), &mut node, 1);
417 assert_eq!(reply, DevReply::Refused(StageRefusal::NotADevNode));
418 assert!(node.calls.is_empty(), "no path from request to the node");
419 assert!(
420 events.is_empty(),
421 "no status invented for a refused request"
422 );
423 }
424
425 #[test]
429 fn activation_runs_the_barrier_in_order() {
430 let mut node = FakeNode::default();
431 let mut engine = engine(NodeMode::Dev);
432 let (reply, events) = drive(&mut engine, &mut node, 1);
433 assert_eq!(
434 node.calls,
435 vec!["stop", "stage:1", "start", "mailbox", "content"]
436 );
437 match reply {
438 DevReply::Activated(report) => assert_eq!(report.build_generation, 1),
439 other => panic!("expected Activated, got {other:?}"),
440 }
441 assert!(matches!(
442 events.last(),
443 Some(DevStatusEvent::RunningCurrent(report)) if report.build_generation == 1
444 ));
445 }
446
447 #[test]
451 fn stage_failures_roll_back_and_reprove_last_good() {
452 type Inject = fn(&mut FakeNode);
453 let scripted: [(Inject, ReloadStage); 3] = [
454 (|n| n.fail_stage = true, ReloadStage::PurgeOld),
455 (
456 |n| n.fail_start = Some(StartFailure::Load("bad beam".to_owned())),
457 ReloadStage::LoadCandidate,
458 ),
459 (
460 |n| n.fail_start = Some(StartFailure::Tree("tree died".to_owned())),
461 ReloadStage::StartFresh,
462 ),
463 ];
464 for (inject, expected_stage) in scripted {
465 let mut node = FakeNode::failing(inject, 1);
466 let mut engine = engine(NodeMode::Dev);
467 let (reply, events) = drive(&mut engine, &mut node, 1);
468 match reply {
469 DevReply::RolledBack {
470 failed, serving, ..
471 } => {
472 assert_eq!(failed, expected_stage);
473 assert_eq!(serving.build_generation, 0, "last-good serves");
474 }
475 other => panic!("expected RolledBack at {expected_stage:?}, got {other:?}"),
476 }
477 let tail: Vec<_> = node.calls.iter().rev().take(4).rev().cloned().collect();
478 assert_eq!(
479 tail,
480 vec!["stage:0", "start", "mailbox", "content"],
481 "restore re-proves last good at {expected_stage:?}"
482 );
483 assert!(matches!(
484 events.last(),
485 Some(DevStatusEvent::ReloadFailed { serving, .. })
486 if serving.build_generation == 0
487 ));
488 }
489 }
490
491 #[test]
495 fn witness_failure_stops_the_unproven_tree_before_restore() {
496 let mut node = FakeNode::failing(|n| n.fail_content = true, 1);
497 let mut engine = engine(NodeMode::Dev);
498 let (reply, _events) = drive(&mut engine, &mut node, 1);
499 match reply {
500 DevReply::RolledBack { failed, .. } => {
501 assert_eq!(failed, ReloadStage::LivenessContent);
502 }
503 other => panic!("expected RolledBack, got {other:?}"),
504 }
505 assert_eq!(
506 node.calls,
507 vec![
508 "stop", "stage:1", "start", "mailbox", "content", "stop", "stage:0", "start", "mailbox", "content" ]
512 );
513 }
514
515 #[test]
518 fn restore_failure_is_node_failed() {
519 let mut node = FakeNode::failing(|n| n.fail_stage = true, 2);
521 let mut engine = engine(NodeMode::Dev);
522 let (reply, events) = drive(&mut engine, &mut node, 1);
523 match reply {
524 DevReply::NodeFailed { failed, detail } => {
525 assert_eq!(failed, ReloadStage::PurgeOld);
526 assert!(detail.contains("restoring last good failed"), "{detail}");
527 }
528 other => panic!("expected NodeFailed, got {other:?}"),
529 }
530 assert!(matches!(
531 events.last(),
532 Some(DevStatusEvent::NodeFailed { .. })
533 ));
534 }
535
536 #[test]
539 fn stale_generations_are_refused_by_identity() {
540 let mut node = FakeNode::failing(|n| n.fail_stage = true, 1);
541 let mut engine = engine(NodeMode::Dev);
542 let (reply, _) = drive(&mut engine, &mut node, 3);
543 assert!(matches!(reply, DevReply::RolledBack { .. }));
544
545 let calls_before = node.calls.len();
547 let (reply, _) = drive(&mut engine, &mut node, 3);
548 assert_eq!(
549 reply,
550 DevReply::Refused(StageRefusal::StaleGeneration {
551 offered: 3,
552 newest_seen: 3
553 })
554 );
555 assert_eq!(node.calls.len(), calls_before);
556
557 let (reply, _) = drive(&mut engine, &mut node, 4);
559 assert!(matches!(reply, DevReply::Activated(_)));
560 }
561}