1use std::collections::{BTreeSet, HashMap};
4use std::sync::{Mutex, MutexGuard};
5
6use crate::{EngineError, Pid};
7
8#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
10pub struct EngineSupervisorId;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct TypeSupervisorId {
15 workflow_type: String,
16}
17
18impl TypeSupervisorId {
19 #[must_use]
21 pub fn workflow_type(&self) -> &str {
22 &self.workflow_type
23 }
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct TypeSupervisorNode {
29 id: TypeSupervisorId,
30 workflow_processes: BTreeSet<Pid>,
31}
32
33impl TypeSupervisorNode {
34 #[must_use]
36 pub fn id(&self) -> &TypeSupervisorId {
37 &self.id
38 }
39
40 #[must_use]
42 pub fn workflow_processes(&self) -> &BTreeSet<Pid> {
43 &self.workflow_processes
44 }
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct WorkflowNode {
50 workflow_type: String,
51 workflow_pid: Pid,
52 activity_children: BTreeSet<Pid>,
53}
54
55impl WorkflowNode {
56 #[must_use]
58 pub fn workflow_type(&self) -> &str {
59 &self.workflow_type
60 }
61
62 #[must_use]
64 pub fn workflow_pid(&self) -> Pid {
65 self.workflow_pid
66 }
67
68 #[must_use]
70 pub fn activity_children(&self) -> &BTreeSet<Pid> {
71 &self.activity_children
72 }
73
74 #[must_use]
76 pub fn has_activity_child(&self, activity_pid: Pid) -> bool {
77 self.activity_children.contains(&activity_pid)
78 }
79}
80
81#[derive(Debug, Default)]
82struct TreeState {
83 type_supervisors: HashMap<String, TypeSupervisorNode>,
84 workflows: HashMap<Pid, WorkflowNode>,
85}
86
87#[derive(Debug, Default)]
95pub struct SupervisionTree {
96 state: Mutex<TreeState>,
97}
98
99impl SupervisionTree {
100 #[must_use]
102 pub fn new() -> Self {
103 Self::default()
104 }
105
106 #[must_use]
108 pub const fn engine_supervisor(&self) -> EngineSupervisorId {
109 EngineSupervisorId
110 }
111
112 pub fn ensure_type_supervisor(
118 &self,
119 workflow_type: impl Into<String>,
120 ) -> Result<TypeSupervisorId, EngineError> {
121 let workflow_type = workflow_type.into();
122 let mut state = self.state()?;
123 let supervisor = state
124 .type_supervisors
125 .entry(workflow_type.clone())
126 .or_insert_with(|| TypeSupervisorNode {
127 id: TypeSupervisorId { workflow_type },
128 workflow_processes: BTreeSet::new(),
129 });
130 Ok(supervisor.id.clone())
131 }
132
133 pub fn place_workflow(
142 &self,
143 workflow_type: impl Into<String>,
144 workflow_pid: Pid,
145 ) -> Result<TypeSupervisorId, EngineError> {
146 let workflow_type = workflow_type.into();
147 let mut state = self.state()?;
148
149 if let Some(existing) = state.workflows.get(&workflow_pid) {
150 if existing.workflow_type != workflow_type {
151 return Err(tree_runtime_error(format!(
152 "workflow process {workflow_pid} is already placed under workflow type `{}`",
153 existing.workflow_type
154 )));
155 }
156
157 let supervisor = state
158 .type_supervisors
159 .entry(workflow_type.clone())
160 .or_insert_with(|| TypeSupervisorNode {
161 id: TypeSupervisorId { workflow_type },
162 workflow_processes: BTreeSet::new(),
163 });
164 supervisor.workflow_processes.insert(workflow_pid);
165 return Ok(supervisor.id.clone());
166 }
167
168 let supervisor = state
169 .type_supervisors
170 .entry(workflow_type.clone())
171 .or_insert_with(|| TypeSupervisorNode {
172 id: TypeSupervisorId {
173 workflow_type: workflow_type.clone(),
174 },
175 workflow_processes: BTreeSet::new(),
176 });
177 supervisor.workflow_processes.insert(workflow_pid);
178 let id = supervisor.id.clone();
179 state.workflows.insert(
180 workflow_pid,
181 WorkflowNode {
182 workflow_type,
183 workflow_pid,
184 activity_children: BTreeSet::new(),
185 },
186 );
187 Ok(id)
188 }
189
190 pub fn record_activity_child(
200 &self,
201 workflow_pid: Pid,
202 activity_pid: Pid,
203 ) -> Result<(), EngineError> {
204 let mut state = self.state()?;
205 if workflow_pid == activity_pid {
206 return Err(tree_runtime_error(format!(
207 "workflow process {workflow_pid} cannot be its own activity child"
208 )));
209 }
210 if state.workflows.contains_key(&activity_pid) {
211 return Err(tree_runtime_error(format!(
212 "process {activity_pid} is already placed as a workflow process"
213 )));
214 }
215 let workflow = state.workflows.get_mut(&workflow_pid).ok_or_else(|| {
216 tree_runtime_error(format!(
217 "workflow process {workflow_pid} is not in the supervision tree"
218 ))
219 })?;
220 workflow.activity_children.insert(activity_pid);
221 Ok(())
222 }
223
224 pub fn type_supervisor_count(&self) -> Result<usize, EngineError> {
230 Ok(self.state()?.type_supervisors.len())
231 }
232
233 pub fn type_supervisors(&self) -> Result<Vec<TypeSupervisorNode>, EngineError> {
239 Ok(self.state()?.type_supervisors.values().cloned().collect())
240 }
241
242 pub fn workflow(&self, workflow_pid: Pid) -> Result<Option<WorkflowNode>, EngineError> {
248 Ok(self.state()?.workflows.get(&workflow_pid).cloned())
249 }
250
251 fn state(&self) -> Result<MutexGuard<'_, TreeState>, EngineError> {
252 self.state.lock().map_err(|_| EngineError::RegistryPoisoned)
253 }
254}
255
256fn tree_runtime_error(reason: String) -> EngineError {
257 EngineError::Runtime { reason }
258}
259
260#[cfg(test)]
261mod tests {
262 use std::sync::Arc;
263
264 use crate::EngineError;
265
266 use super::SupervisionTree;
267
268 #[test]
269 fn one_engine_root_has_one_supervisor_per_workflow_type() -> Result<(), EngineError> {
270 let tree = SupervisionTree::new();
271 let root = tree.engine_supervisor();
272 let checkout = tree.ensure_type_supervisor("checkout")?;
273 let billing = tree.ensure_type_supervisor("billing")?;
274 let checkout_again = tree.ensure_type_supervisor("checkout")?;
275
276 assert_eq!(root, tree.engine_supervisor());
277 assert_eq!(checkout.workflow_type(), "checkout");
278 assert_eq!(billing.workflow_type(), "billing");
279 assert_eq!(checkout, checkout_again);
280 assert_eq!(tree.type_supervisor_count()?, 2);
281 Ok(())
282 }
283
284 #[test]
285 fn workflows_sit_under_type_supervisors_not_new_supervisors() -> Result<(), EngineError> {
286 let tree = SupervisionTree::new();
287
288 tree.place_workflow("checkout", 10)?;
289 tree.place_workflow("checkout", 11)?;
290
291 assert_eq!(tree.type_supervisor_count()?, 1);
292 let supervisors = tree.type_supervisors()?;
293 let checkout = supervisors
294 .iter()
295 .find(|node| node.id().workflow_type() == "checkout");
296 assert!(checkout.is_some());
297 if let Some(checkout) = checkout {
298 assert_eq!(checkout.workflow_processes().len(), 2);
299 assert!(checkout.workflow_processes().contains(&10));
300 assert!(checkout.workflow_processes().contains(&11));
301 }
302 Ok(())
303 }
304
305 #[test]
306 fn activity_children_are_one_level_below_workflow_process() -> Result<(), EngineError> {
307 let tree = SupervisionTree::new();
308
309 tree.place_workflow("checkout", 10)?;
310 tree.record_activity_child(10, 20)?;
311 tree.record_activity_child(10, 21)?;
312
313 let workflow = tree.workflow(10)?;
314 assert!(workflow.is_some());
315 if let Some(workflow) = workflow {
316 assert_eq!(workflow.workflow_type(), "checkout");
317 assert_eq!(workflow.workflow_pid(), 10);
318 assert!(workflow.has_activity_child(20));
319 assert!(workflow.has_activity_child(21));
320 assert_eq!(workflow.activity_children().len(), 2);
321 }
322 Ok(())
323 }
324
325 #[test]
326 fn missing_workflow_activity_parent_is_typed_error() {
327 let tree = SupervisionTree::new();
328
329 let error = tree.record_activity_child(99, 20);
330
331 assert!(matches!(error, Err(EngineError::Runtime { .. })));
332 }
333
334 #[test]
335 fn placing_existing_workflow_is_idempotent_and_preserves_children() -> Result<(), EngineError> {
336 let tree = SupervisionTree::new();
337
338 let first = tree.place_workflow("checkout", 10)?;
339 tree.record_activity_child(10, 20)?;
340 let second = tree.place_workflow("checkout", 10)?;
341
342 assert_eq!(first, second);
343 assert_eq!(tree.type_supervisor_count()?, 1);
344 let workflow = tree.workflow(10)?;
345 assert!(workflow.is_some());
346 if let Some(workflow) = workflow {
347 assert!(workflow.has_activity_child(20));
348 }
349 Ok(())
350 }
351
352 #[test]
353 fn workflow_pid_cannot_move_between_type_supervisors() -> Result<(), EngineError> {
354 let tree = SupervisionTree::new();
355
356 tree.place_workflow("checkout", 10)?;
357 let error = tree.place_workflow("billing", 10);
358
359 assert!(matches!(error, Err(EngineError::Runtime { .. })));
360 assert_eq!(tree.type_supervisor_count()?, 1);
361 Ok(())
362 }
363
364 #[test]
365 fn activity_child_cannot_alias_workflow_process() -> Result<(), EngineError> {
366 let tree = SupervisionTree::new();
367
368 tree.place_workflow("checkout", 10)?;
369 assert!(matches!(
370 tree.record_activity_child(10, 10),
371 Err(EngineError::Runtime { .. })
372 ));
373 Ok(())
374 }
375
376 #[test]
377 fn poisoned_tree_lock_returns_typed_registry_error() {
378 let tree = Arc::new(SupervisionTree::new());
379 let poisoner_tree = Arc::clone(&tree);
380 let poisoner = std::thread::spawn(move || {
381 let guard = poisoner_tree.state.lock();
382 assert!(guard.is_ok());
383 std::panic::resume_unwind(Box::new("poison supervision tree lock"));
384 });
385
386 assert!(poisoner.join().is_err());
387 assert!(matches!(
388 tree.type_supervisor_count(),
389 Err(EngineError::RegistryPoisoned)
390 ));
391 }
392}