1use std::sync::atomic::{AtomicBool, Ordering};
22use std::time::Duration;
23
24use frame_conv::id::CorrelationId;
25
26use super::{BarrierEngine, DevReply, DevRequest, DevStatusEvent, NodeControl};
27
28pub struct InboundManagement {
30 pub request: DevRequest,
32 pub correlation: CorrelationId,
34}
35
36pub trait ManagementConversation {
41 fn next_request(&mut self, wait: Duration) -> Result<Option<InboundManagement>, String>;
48 fn reply(&mut self, correlation: CorrelationId, reply: &DevReply) -> Result<(), String>;
54 fn publish_status(&mut self, event: &DevStatusEvent) -> Result<(), String>;
60}
61
62#[derive(Debug, PartialEq, Eq)]
64pub enum ServeExit {
65 Closed,
67 ConnectionLost {
69 detail: String,
71 },
72}
73
74pub fn serve<C: ManagementConversation>(
81 conversation: &mut C,
82 engine: &mut BarrierEngine,
83 control: &mut dyn NodeControl,
84 close: &AtomicBool,
85 quantum: Duration,
86) -> ServeExit {
87 loop {
88 if close.load(Ordering::Acquire) {
89 return ServeExit::Closed;
90 }
91 match conversation.next_request(quantum) {
92 Ok(None) => {}
95 Ok(Some(inbound)) => {
96 let mut publish_failure: Option<String> = None;
97 let reply = {
98 let failure = &mut publish_failure;
99 engine.handle(inbound.request, control, &mut |event| {
100 if failure.is_none()
101 && let Err(detail) = conversation_publish(conversation, &event)
102 {
103 *failure = Some(detail);
104 }
105 })
106 };
107 if let Err(detail) = conversation.reply(inbound.correlation, &reply) {
108 return ServeExit::ConnectionLost { detail };
109 }
110 if let Some(detail) = publish_failure {
111 return ServeExit::ConnectionLost { detail };
112 }
113 }
114 Err(detail) => return ServeExit::ConnectionLost { detail },
115 }
116 }
117}
118
119fn conversation_publish<C: ManagementConversation>(
122 conversation: &mut C,
123 event: &DevStatusEvent,
124) -> Result<(), String> {
125 conversation.publish_status(event)
126}
127
128#[cfg(test)]
129mod tests {
130 #![allow(clippy::expect_used, clippy::panic)]
131
132 use std::sync::atomic::AtomicBool;
133 use std::time::Duration;
134
135 use frame_conv::id::CorrelationId;
136
137 use super::{InboundManagement, ManagementConversation, ServeExit, serve};
138 use crate::dev::{
139 BarrierEngine, CandidateBytes, DevReply, DevRequest, DevStatusEvent, GenerationReport,
140 NodeControl, NodeMode, StageRefusal, StartFailure,
141 };
142
143 const QUANTUM: Duration = Duration::from_millis(1);
144
145 struct FakeConversation {
148 script: Vec<Option<InboundManagement>>,
149 replies: Vec<DevReply>,
150 published: Vec<DevStatusEvent>,
151 pump_ticks: usize,
152 }
153
154 impl FakeConversation {
155 fn new(mut script: Vec<Option<InboundManagement>>) -> Self {
156 script.reverse();
157 Self {
158 script,
159 replies: Vec::new(),
160 published: Vec::new(),
161 pump_ticks: 0,
162 }
163 }
164 }
165
166 impl ManagementConversation for FakeConversation {
167 fn next_request(&mut self, _wait: Duration) -> Result<Option<InboundManagement>, String> {
168 self.pump_ticks += 1;
169 match self.script.pop() {
170 Some(entry) => Ok(entry),
171 None => Err("script exhausted: connection closed".to_owned()),
172 }
173 }
174 fn reply(&mut self, _correlation: CorrelationId, reply: &DevReply) -> Result<(), String> {
175 self.replies.push(reply.clone());
176 Ok(())
177 }
178 fn publish_status(&mut self, event: &DevStatusEvent) -> Result<(), String> {
179 self.published.push(event.clone());
180 Ok(())
181 }
182 }
183
184 #[derive(Default)]
187 struct CountingNode {
188 operations: usize,
189 }
190
191 impl NodeControl for CountingNode {
192 fn stop_component(&mut self) -> Result<(), String> {
193 self.operations += 1;
194 Ok(())
195 }
196 fn stage(&mut self, _candidate: &CandidateBytes) -> Result<(), String> {
197 self.operations += 1;
198 Ok(())
199 }
200 fn start_component(&mut self) -> Result<(), StartFailure> {
201 self.operations += 1;
202 Ok(())
203 }
204 fn witness_mailbox(&mut self) -> Result<(), String> {
205 self.operations += 1;
206 Ok(())
207 }
208 fn witness_content(&mut self) -> Result<(), String> {
209 self.operations += 1;
210 Ok(())
211 }
212 fn report(
213 &mut self,
214 build_generation: u64,
215 content_digest: [u8; 32],
216 ) -> Result<GenerationReport, String> {
217 self.operations += 1;
218 Ok(GenerationReport {
219 build_generation,
220 content_digest,
221 component_incarnation: 1,
222 module_generation: 1,
223 })
224 }
225 }
226
227 fn candidate(generation: u64) -> CandidateBytes {
228 CandidateBytes {
229 build_generation: generation,
230 content_digest: [0; 32],
231 component_beam: vec![1],
232 ffi_beam: vec![2],
233 }
234 }
235
236 fn inbound(generation: u64) -> InboundManagement {
237 InboundManagement {
238 request: DevRequest::StageAndActivate(candidate(generation)),
239 correlation: CorrelationId::mint(),
240 }
241 }
242
243 #[test]
247 fn quiet_ticks_do_no_work() {
248 let mut conversation = FakeConversation::new(vec![None, None, None, None, None]);
249 let mut engine = BarrierEngine::new(NodeMode::Dev, candidate(0));
250 let mut node = CountingNode::default();
251 let close = AtomicBool::new(false);
252
253 let exit = serve(&mut conversation, &mut engine, &mut node, &close, QUANTUM);
254 assert!(matches!(exit, ServeExit::ConnectionLost { .. }));
256 assert_eq!(conversation.pump_ticks, 6, "five quiet ticks + the fate");
257 assert_eq!(node.operations, 0, "quiet ticks touch the node ZERO times");
258 assert!(
259 conversation.published.is_empty(),
260 "quiet ticks publish nothing"
261 );
262 assert!(
263 conversation.replies.is_empty(),
264 "quiet ticks reply to nothing"
265 );
266 }
267
268 #[test]
271 fn requests_are_served_between_quiet_ticks() {
272 let mut conversation = FakeConversation::new(vec![None, Some(inbound(1)), None]);
273 let mut engine = BarrierEngine::new(NodeMode::Dev, candidate(0));
274 let mut node = CountingNode::default();
275 let close = AtomicBool::new(false);
276
277 let exit = serve(&mut conversation, &mut engine, &mut node, &close, QUANTUM);
278 assert!(matches!(exit, ServeExit::ConnectionLost { .. }));
279 assert_eq!(conversation.replies.len(), 1, "exactly one reply");
280 assert!(matches!(conversation.replies[0], DevReply::Activated(_)));
281 assert!(
282 matches!(
283 conversation.published.last(),
284 Some(DevStatusEvent::RunningCurrent(_))
285 ),
286 "the barrier's status transitions were published"
287 );
288 }
289
290 #[test]
293 fn close_flag_exits_ordered() {
294 let mut conversation = FakeConversation::new(vec![None]);
295 let mut engine = BarrierEngine::new(NodeMode::Dev, candidate(0));
296 let mut node = CountingNode::default();
297 let close = AtomicBool::new(true);
298
299 let exit = serve(&mut conversation, &mut engine, &mut node, &close, QUANTUM);
300 assert_eq!(exit, ServeExit::Closed);
301 assert_eq!(conversation.pump_ticks, 0, "no pump arm after close");
302 }
303
304 #[test]
307 fn production_refusal_holds_through_the_adapter() {
308 let mut conversation = FakeConversation::new(vec![Some(inbound(1))]);
309 let mut engine = BarrierEngine::new(NodeMode::Production, candidate(0));
310 let mut node = CountingNode::default();
311 let close = AtomicBool::new(false);
312
313 let _exit = serve(&mut conversation, &mut engine, &mut node, &close, QUANTUM);
314 assert_eq!(
315 conversation.replies[0],
316 DevReply::Refused(StageRefusal::NotADevNode)
317 );
318 assert_eq!(node.operations, 0);
319 assert!(conversation.published.is_empty());
320 }
321}