Skip to main content

fsqlite_core/
region.rs

1//! Structured concurrency region tree (§4.11, bd-3go.9).
2//!
3//! Every background worker, coordinator, and long-lived service runs as a
4//! region-owned task. The region tree enforces INV-REGION-QUIESCENCE: no
5//! region closes until all children complete, all finalizers run, and all
6//! obligations resolve.
7//!
8//! Close protocol: cancel → drain children → run finalizers → mark closed.
9
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicUsize, Ordering};
13
14use fsqlite_error::{FrankenError, Result};
15use fsqlite_types::Region;
16use fsqlite_types::cx::{self, Cx};
17use tracing::debug;
18
19use crate::quiescence::{ChildRegionQuiescence, RegionQuiescenceSnapshot};
20
21// ── Types ──────────────────────────────────────────────────────────────
22
23/// Normative region kinds from §4.11.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum RegionKind {
26    /// Top-level database root region.
27    DbRoot,
28    /// Write coordinator service region (native marker sequencer + compat WAL).
29    WriteCoordinator,
30    /// Symbol store service region (local symbol logs + tiered storage fetch).
31    SymbolStore,
32    /// Replication service region (stream symbols; anti-entropy; membership).
33    Replication,
34    /// Checkpoint/GC service region (checkpointer, compactor, GC horizon).
35    CheckpointGc,
36    /// Observability service region (deadline monitor, task inspector, metrics).
37    Observability,
38    /// Per-connection region (child of root).
39    PerConnection,
40    /// Per-transaction region (child of connection).
41    PerTransaction,
42}
43
44/// State machine for region lifecycle.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum RegionState {
47    /// Region is open and accepting work.
48    Open,
49    /// Region is closing (cancellation requested, draining).
50    Closing,
51    /// Region is fully closed (quiescent, finalizers run).
52    Closed,
53}
54
55/// A finalizer callback to run during region close.
56type Finalizer = Box<dyn FnOnce() + Send>;
57
58/// Shared atomic counter for tracking active tasks or obligations.
59type SharedCounter = Arc<AtomicUsize>;
60
61fn new_counter() -> SharedCounter {
62    Arc::new(AtomicUsize::new(0))
63}
64
65// ── RegionNode ─────────────────────────────────────────────────────────
66
67/// A node in the structured concurrency region tree.
68struct RegionNode {
69    kind: RegionKind,
70    state: RegionState,
71    cx: Cx<cx::FullCaps>,
72    parent: Option<Region>,
73    children: Vec<Region>,
74    finalizers: Vec<Finalizer>,
75    active_tasks: SharedCounter,
76    active_obligations: SharedCounter,
77}
78
79// ── RAII handles ───────────────────────────────────────────────────────
80
81/// RAII handle for a task registered in a region.
82///
83/// When dropped, the task count for the owning region is decremented.
84/// This ensures tasks cannot leak without being accounted for.
85pub struct TaskHandle {
86    counter: SharedCounter,
87    region: Region,
88}
89
90impl TaskHandle {
91    /// The region this task belongs to.
92    #[must_use]
93    pub const fn region(&self) -> Region {
94        self.region
95    }
96}
97
98impl Drop for TaskHandle {
99    fn drop(&mut self) {
100        self.counter.fetch_sub(1, Ordering::AcqRel);
101    }
102}
103
104/// RAII handle for an obligation registered in a region.
105///
106/// When dropped (resolved), the obligation count is decremented.
107/// Obligations model the two-phase lifecycle: Reserved → Committed/Aborted.
108pub struct ObligationHandle {
109    counter: SharedCounter,
110    region: Region,
111}
112
113impl ObligationHandle {
114    /// The region this obligation belongs to.
115    #[must_use]
116    pub const fn region(&self) -> Region {
117        self.region
118    }
119
120    /// Explicitly resolve the obligation (commit or abort).
121    ///
122    /// Equivalent to dropping the handle; provided for clarity at call sites.
123    pub fn resolve(self) {
124        // Ownership transfer triggers Drop, which decrements the counter.
125    }
126}
127
128impl Drop for ObligationHandle {
129    fn drop(&mut self) {
130        self.counter.fetch_sub(1, Ordering::AcqRel);
131    }
132}
133
134// ── RegionTree ─────────────────────────────────────────────────────────
135
136/// Tree of regions enforcing structured concurrency (§4.11).
137///
138/// Every task/actor must be region-owned. The tree enforces
139/// INV-REGION-QUIESCENCE: no region closes until all children
140/// complete, all finalizers run, and all obligations resolve.
141pub struct RegionTree {
142    nodes: HashMap<Region, RegionNode>,
143    next_id: u32,
144    root: Option<Region>,
145}
146
147impl Default for RegionTree {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl RegionTree {
154    /// Create an empty region tree.
155    #[must_use]
156    pub fn new() -> Self {
157        Self {
158            nodes: HashMap::new(),
159            next_id: 0,
160            root: None,
161        }
162    }
163
164    /// Create the root region.
165    ///
166    /// Only one root region may exist. Returns an error if a root already exists.
167    pub fn create_root(&mut self, kind: RegionKind, cx: Cx<cx::FullCaps>) -> Result<Region> {
168        if self.root.is_some() {
169            return Err(FrankenError::Internal(
170                "root region already exists".to_owned(),
171            ));
172        }
173        let id = self.alloc_id();
174        self.nodes.insert(
175            id,
176            RegionNode {
177                kind,
178                state: RegionState::Open,
179                cx,
180                parent: None,
181                children: Vec::new(),
182                finalizers: Vec::new(),
183                active_tasks: new_counter(),
184                active_obligations: new_counter(),
185            },
186        );
187        self.root = Some(id);
188        debug!(region = id.get(), kind = ?kind, "region created (root)");
189        Ok(id)
190    }
191
192    /// Create a child region under the given parent.
193    pub fn create_child(
194        &mut self,
195        parent: Region,
196        kind: RegionKind,
197        cx: Cx<cx::FullCaps>,
198    ) -> Result<Region> {
199        let parent_state = self.nodes.get(&parent).map(|n| n.state).ok_or_else(|| {
200            FrankenError::Internal(format!("parent region {} not found", parent.get()))
201        })?;
202        if parent_state != RegionState::Open {
203            return Err(FrankenError::Busy);
204        }
205        let id = self.alloc_id();
206        self.nodes.insert(
207            id,
208            RegionNode {
209                kind,
210                state: RegionState::Open,
211                cx,
212                parent: Some(parent),
213                children: Vec::new(),
214                finalizers: Vec::new(),
215                active_tasks: new_counter(),
216                active_obligations: new_counter(),
217            },
218        );
219        if let Some(parent_node) = self.nodes.get_mut(&parent) {
220            parent_node.children.push(id);
221        }
222        debug!(region = id.get(), parent = parent.get(), kind = ?kind, "region created (child)");
223        Ok(id)
224    }
225
226    // ── Accessors ──────────────────────────────────────────────────────
227
228    /// Root region, if created.
229    #[must_use]
230    pub fn root(&self) -> Option<Region> {
231        self.root
232    }
233
234    /// Query the kind of a region.
235    #[must_use]
236    pub fn kind(&self, id: Region) -> Option<RegionKind> {
237        self.nodes.get(&id).map(|n| n.kind)
238    }
239
240    /// Query the state of a region.
241    #[must_use]
242    pub fn state(&self, id: Region) -> Option<RegionState> {
243        self.nodes.get(&id).map(|n| n.state)
244    }
245
246    /// Query the parent of a region.
247    #[must_use]
248    pub fn parent(&self, id: Region) -> Option<Option<Region>> {
249        self.nodes.get(&id).map(|n| n.parent)
250    }
251
252    /// List children of a region.
253    #[must_use]
254    pub fn children(&self, id: Region) -> Option<&[Region]> {
255        self.nodes.get(&id).map(|n| n.children.as_slice())
256    }
257
258    /// Get a clone of the region's `Cx` for cancellation inspection.
259    #[must_use]
260    pub fn cx(&self, id: Region) -> Option<Cx<cx::FullCaps>> {
261        self.nodes.get(&id).map(|n| n.cx.clone())
262    }
263
264    /// Active task count for a region.
265    #[must_use]
266    pub fn active_tasks(&self, id: Region) -> usize {
267        self.nodes
268            .get(&id)
269            .map_or(0, |n| n.active_tasks.load(Ordering::Acquire))
270    }
271
272    /// Active obligation count for a region.
273    #[must_use]
274    pub fn active_obligations(&self, id: Region) -> usize {
275        self.nodes
276            .get(&id)
277            .map_or(0, |n| n.active_obligations.load(Ordering::Acquire))
278    }
279
280    // ── Task / obligation / finalizer registration ─────────────────────
281
282    /// Register a task in a region, returning an RAII handle.
283    ///
284    /// The task count is incremented; when the handle is dropped, it decrements.
285    /// Returns `Err(Busy)` if the region is not [`RegionState::Open`].
286    pub fn register_task(&self, id: Region) -> Result<TaskHandle> {
287        let node = self
288            .nodes
289            .get(&id)
290            .ok_or_else(|| FrankenError::Internal(format!("region {} not found", id.get())))?;
291        if node.state != RegionState::Open {
292            return Err(FrankenError::Busy);
293        }
294        node.active_tasks.fetch_add(1, Ordering::AcqRel);
295        debug!(region = id.get(), "task registered");
296        Ok(TaskHandle {
297            counter: Arc::clone(&node.active_tasks),
298            region: id,
299        })
300    }
301
302    /// Register an obligation in a region, returning an RAII handle.
303    ///
304    /// Obligations can be registered while the region is Open or Closing
305    /// (to allow in-flight work to create follow-up obligations during drain).
306    /// Returns `Err(Busy)` if the region is [`RegionState::Closed`].
307    pub fn register_obligation(&self, id: Region) -> Result<ObligationHandle> {
308        let node = self
309            .nodes
310            .get(&id)
311            .ok_or_else(|| FrankenError::Internal(format!("region {} not found", id.get())))?;
312        if node.state == RegionState::Closed {
313            return Err(FrankenError::Busy);
314        }
315        node.active_obligations.fetch_add(1, Ordering::AcqRel);
316        debug!(region = id.get(), "obligation registered");
317        Ok(ObligationHandle {
318            counter: Arc::clone(&node.active_obligations),
319            region: id,
320        })
321    }
322
323    /// Register a finalizer callback to run during region close.
324    pub fn register_finalizer(
325        &mut self,
326        id: Region,
327        finalizer: impl FnOnce() + Send + 'static,
328    ) -> Result<()> {
329        let node = self
330            .nodes
331            .get_mut(&id)
332            .ok_or_else(|| FrankenError::Internal(format!("region {} not found", id.get())))?;
333        if node.state != RegionState::Open {
334            return Err(FrankenError::Busy);
335        }
336        node.finalizers.push(Box::new(finalizer));
337        Ok(())
338    }
339
340    // ── Close protocol ─────────────────────────────────────────────────
341
342    /// Begin closing a region: cancel its `Cx` and set state to `Closing`.
343    ///
344    /// Recursively begins close on all descendant regions (parent-first
345    /// cancellation propagation per INV-CANCEL-PROPAGATES).
346    ///
347    /// Does NOT wait for quiescence. Use [`is_quiescent`](Self::is_quiescent)
348    /// to poll, then [`complete_close`](Self::complete_close) to finalize.
349    pub fn begin_close(&mut self, id: Region) -> Result<()> {
350        let children = self
351            .nodes
352            .get(&id)
353            .ok_or_else(|| FrankenError::Internal(format!("region {} not found", id.get())))?
354            .children
355            .clone();
356
357        // Cancel this region's Cx first (parent-first propagation).
358        let node = self
359            .nodes
360            .get_mut(&id)
361            .expect("region confirmed present above");
362        if node.state == RegionState::Closed {
363            return Ok(());
364        }
365        node.cx.cancel();
366        node.state = RegionState::Closing;
367        debug!(region = id.get(), kind = ?node.kind, "region closing");
368
369        // Then recursively close children.
370        for child in children {
371            if self.state(child) == Some(RegionState::Open) {
372                self.begin_close(child)?;
373            }
374        }
375        Ok(())
376    }
377
378    /// Check whether a region has reached quiescence.
379    ///
380    /// A region is quiescent when:
381    /// - all child regions are [`RegionState::Closed`],
382    /// - active task count is zero,
383    /// - active obligation count is zero.
384    #[must_use]
385    pub fn is_quiescent(&self, id: Region) -> bool {
386        self.quiescence_snapshot(id)
387            .is_some_and(|snapshot| snapshot.is_quiescent())
388    }
389
390    /// Capture a deterministic quiescence snapshot for diagnostics and tests.
391    #[must_use]
392    pub fn quiescence_snapshot(&self, id: Region) -> Option<RegionQuiescenceSnapshot> {
393        let node = self.nodes.get(&id)?;
394        let non_closed_children = node
395            .children
396            .iter()
397            .filter_map(|child| {
398                let state = self.state(*child);
399                if state == Some(RegionState::Closed) {
400                    None
401                } else {
402                    Some(ChildRegionQuiescence {
403                        region: *child,
404                        state,
405                    })
406                }
407            })
408            .collect();
409
410        Some(RegionQuiescenceSnapshot {
411            region: id,
412            state: node.state,
413            active_tasks: node.active_tasks.load(Ordering::Acquire),
414            active_obligations: node.active_obligations.load(Ordering::Acquire),
415            non_closed_children,
416        })
417    }
418
419    /// Complete region close: run finalizers and mark as [`RegionState::Closed`].
420    ///
421    /// Returns an error if the region is not quiescent.
422    pub fn complete_close(&mut self, id: Region) -> Result<()> {
423        let state = self
424            .nodes
425            .get(&id)
426            .map(|n| n.state)
427            .ok_or_else(|| FrankenError::Internal(format!("region {} not found", id.get())))?;
428        if state == RegionState::Closed {
429            return Ok(());
430        }
431        if state != RegionState::Closing {
432            return Err(FrankenError::Internal(
433                "region must be in Closing state before complete_close".to_owned(),
434            ));
435        }
436        let snapshot = self
437            .quiescence_snapshot(id)
438            .ok_or_else(|| FrankenError::Internal(format!("region {} not found", id.get())))?;
439        if !snapshot.is_quiescent() {
440            return Err(FrankenError::Internal(format!(
441                "region not quiescent; children_open={} active_tasks={} active_obligations={}",
442                snapshot.non_closed_children.len(),
443                snapshot.active_tasks,
444                snapshot.active_obligations
445            )));
446        }
447        let node = self
448            .nodes
449            .get_mut(&id)
450            .ok_or_else(|| FrankenError::Internal(format!("region {} not found", id.get())))?;
451        let finalizers = std::mem::take(&mut node.finalizers);
452        for f in finalizers {
453            f();
454        }
455        node.state = RegionState::Closed;
456        debug!(region = id.get(), kind = ?node.kind, "region closed");
457        Ok(())
458    }
459
460    /// Close a region and spin-wait until quiescent, then finalize.
461    ///
462    /// This is the full close protocol: cancel → drain → finalize.
463    /// Blocks the caller until INV-REGION-QUIESCENCE is satisfied.
464    /// Children are drained bottom-up before the parent.
465    pub fn close_and_drain(&mut self, id: Region) -> Result<()> {
466        self.begin_close(id)?;
467        self.drain_subtree(id)
468    }
469
470    /// Recursively drain a subtree bottom-up: wait for each region's tasks
471    /// and obligations to complete, then run finalizers and mark closed.
472    fn drain_subtree(&mut self, id: Region) -> Result<()> {
473        let children = self
474            .nodes
475            .get(&id)
476            .map(|n| n.children.clone())
477            .unwrap_or_default();
478        for child in children {
479            self.drain_subtree(child)?;
480        }
481        while self.active_tasks(id) > 0 || self.active_obligations(id) > 0 {
482            std::hint::spin_loop();
483        }
484        self.complete_close(id)
485    }
486
487    fn alloc_id(&mut self) -> Region {
488        let id = Region::new(self.next_id);
489        self.next_id = self.next_id.checked_add(1).expect("region id overflow");
490        id
491    }
492}
493
494// ── Tests ──────────────────────────────────────────────────────────────
495
496#[cfg(test)]
497mod tests {
498    use std::sync::Arc;
499    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
500
501    use super::*;
502
503    const BEAD_ID: &str = "bd-3go.9";
504
505    #[test]
506    fn test_region_tree_structure() {
507        let mut tree = RegionTree::new();
508        let root = tree
509            .create_root(RegionKind::DbRoot, Cx::new())
510            .expect("root creation");
511        let wc = tree
512            .create_child(root, RegionKind::WriteCoordinator, Cx::new())
513            .expect("wc");
514        let ss = tree
515            .create_child(root, RegionKind::SymbolStore, Cx::new())
516            .expect("ss");
517        let repl = tree
518            .create_child(root, RegionKind::Replication, Cx::new())
519            .expect("repl");
520        let gc = tree
521            .create_child(root, RegionKind::CheckpointGc, Cx::new())
522            .expect("gc");
523        let obs = tree
524            .create_child(root, RegionKind::Observability, Cx::new())
525            .expect("obs");
526
527        // Verify root.
528        assert_eq!(
529            tree.root(),
530            Some(root),
531            "bead_id={BEAD_ID} case=root_exists"
532        );
533        assert_eq!(tree.kind(root), Some(RegionKind::DbRoot));
534
535        // Verify children of root.
536        let children = tree.children(root).expect("root has children");
537        assert_eq!(
538            children.len(),
539            5,
540            "bead_id={BEAD_ID} case=root_has_5_service_children"
541        );
542        assert_eq!(children, &[wc, ss, repl, gc, obs]);
543
544        // Verify each child's kind and parent.
545        assert_eq!(tree.kind(wc), Some(RegionKind::WriteCoordinator));
546        assert_eq!(tree.kind(ss), Some(RegionKind::SymbolStore));
547        assert_eq!(tree.kind(repl), Some(RegionKind::Replication));
548        assert_eq!(tree.kind(gc), Some(RegionKind::CheckpointGc));
549        assert_eq!(tree.kind(obs), Some(RegionKind::Observability));
550
551        for &child in children {
552            assert_eq!(
553                tree.parent(child),
554                Some(Some(root)),
555                "bead_id={BEAD_ID} case=child_parent_is_root region={}",
556                child.get()
557            );
558        }
559    }
560
561    #[test]
562    fn test_region_quiescence_all_children_complete() {
563        let mut tree = RegionTree::new();
564        let root = tree
565            .create_root(RegionKind::DbRoot, Cx::new())
566            .expect("root");
567        let region = tree
568            .create_child(root, RegionKind::WriteCoordinator, Cx::new())
569            .expect("wc");
570
571        // Register 5 tasks.
572        let tasks: Vec<TaskHandle> = (0..5)
573            .map(|_| tree.register_task(region).expect("register task"))
574            .collect();
575
576        assert_eq!(
577            tree.active_tasks(region),
578            5,
579            "bead_id={BEAD_ID} case=5_tasks_registered"
580        );
581
582        // Begin close.
583        tree.begin_close(region).expect("begin close");
584        assert_eq!(tree.state(region), Some(RegionState::Closing));
585        assert!(
586            !tree.is_quiescent(region),
587            "bead_id={BEAD_ID} case=not_quiescent_with_active_tasks"
588        );
589
590        // Complete tasks one by one; quiescence only after all 5.
591        for (i, task) in tasks.into_iter().enumerate() {
592            drop(task);
593            if i < 4 {
594                assert!(
595                    !tree.is_quiescent(region),
596                    "bead_id={BEAD_ID} case=not_quiescent_after_{}_completions",
597                    i + 1
598                );
599            }
600        }
601
602        assert!(
603            tree.is_quiescent(region),
604            "bead_id={BEAD_ID} case=quiescent_after_all_tasks_complete"
605        );
606        tree.complete_close(region).expect("complete close");
607        assert_eq!(tree.state(region), Some(RegionState::Closed));
608    }
609
610    #[test]
611    fn test_region_quiescence_finalizers_run() {
612        let mut tree = RegionTree::new();
613        let root = tree
614            .create_root(RegionKind::DbRoot, Cx::new())
615            .expect("root");
616        let region = tree
617            .create_child(root, RegionKind::WriteCoordinator, Cx::new())
618            .expect("wc");
619
620        // Register 3 tasks with corresponding finalizers.
621        let flags: Vec<Arc<AtomicBool>> =
622            (0..3).map(|_| Arc::new(AtomicBool::new(false))).collect();
623        let tasks: Vec<TaskHandle> = (0..3)
624            .map(|_| tree.register_task(region).expect("register task"))
625            .collect();
626        for flag in &flags {
627            let f = Arc::clone(flag);
628            tree.register_finalizer(region, move || {
629                f.store(true, Ordering::Release);
630            })
631            .expect("register finalizer");
632        }
633
634        // Begin close and complete tasks.
635        tree.begin_close(region).expect("begin close");
636        drop(tasks);
637
638        // Finalizers have NOT run yet.
639        for (i, flag) in flags.iter().enumerate() {
640            assert!(
641                !flag.load(Ordering::Acquire),
642                "bead_id={BEAD_ID} case=finalizer_{i}_not_run_before_complete_close"
643            );
644        }
645
646        // complete_close runs finalizers.
647        tree.complete_close(region).expect("complete close");
648        for (i, flag) in flags.iter().enumerate() {
649            assert!(
650                flag.load(Ordering::Acquire),
651                "bead_id={BEAD_ID} case=finalizer_{i}_ran_after_complete_close"
652            );
653        }
654    }
655
656    #[test]
657    fn test_region_quiescence_obligations_resolved() {
658        let mut tree = RegionTree::new();
659        let root = tree
660            .create_root(RegionKind::DbRoot, Cx::new())
661            .expect("root");
662        let region = tree
663            .create_child(root, RegionKind::WriteCoordinator, Cx::new())
664            .expect("wc");
665
666        let obligations: Vec<ObligationHandle> = (0..3)
667            .map(|_| {
668                tree.register_obligation(region)
669                    .expect("register obligation")
670            })
671            .collect();
672
673        tree.begin_close(region).expect("begin close");
674        assert!(
675            !tree.is_quiescent(region),
676            "bead_id={BEAD_ID} case=not_quiescent_with_pending_obligations"
677        );
678
679        // Resolve obligations one by one.
680        for (i, obligation) in obligations.into_iter().enumerate() {
681            obligation.resolve();
682            if i < 2 {
683                assert!(
684                    !tree.is_quiescent(region),
685                    "bead_id={BEAD_ID} case=not_quiescent_after_{}_resolutions",
686                    i + 1
687                );
688            }
689        }
690
691        assert!(
692            tree.is_quiescent(region),
693            "bead_id={BEAD_ID} case=quiescent_after_all_obligations_resolved"
694        );
695        tree.complete_close(region).expect("complete close");
696    }
697
698    #[test]
699    fn test_quiescence_snapshot_reports_precise_blockers() {
700        let mut tree = RegionTree::new();
701        let root = tree
702            .create_root(RegionKind::DbRoot, Cx::new())
703            .expect("root");
704        let parent = tree
705            .create_child(root, RegionKind::WriteCoordinator, Cx::new())
706            .expect("parent");
707        let child = tree
708            .create_child(parent, RegionKind::PerConnection, Cx::new())
709            .expect("child");
710        let _task = tree.register_task(parent).expect("task");
711        let _obligation = tree.register_obligation(parent).expect("obligation");
712
713        tree.begin_close(parent).expect("begin close");
714        let snapshot = tree
715            .quiescence_snapshot(parent)
716            .expect("snapshot for existing region");
717        assert_eq!(snapshot.region, parent);
718        assert_eq!(snapshot.state, RegionState::Closing);
719        assert_eq!(snapshot.active_tasks, 1);
720        assert_eq!(snapshot.active_obligations, 1);
721        assert_eq!(snapshot.non_closed_children.len(), 1);
722        assert_eq!(snapshot.non_closed_children[0].region, child);
723        assert_eq!(
724            snapshot.non_closed_children[0].state,
725            Some(RegionState::Closing)
726        );
727        assert_eq!(snapshot.blocker_count(), 3);
728        assert!(!snapshot.is_quiescent());
729    }
730
731    #[test]
732    fn test_complete_close_error_includes_quiescence_counts() {
733        let mut tree = RegionTree::new();
734        let root = tree
735            .create_root(RegionKind::DbRoot, Cx::new())
736            .expect("root");
737        let child = tree
738            .create_child(root, RegionKind::WriteCoordinator, Cx::new())
739            .expect("child");
740        let _task = tree.register_task(child).expect("task");
741
742        tree.begin_close(child).expect("begin close");
743        let err = tree
744            .complete_close(child)
745            .expect_err("active task must block close");
746        let FrankenError::Internal(message) = err else {
747            panic!("expected internal error for non-quiescent close");
748        };
749        assert!(
750            message.contains("children_open=0"),
751            "bead_id={BEAD_ID} case=quiescence_error_children_count message={message}"
752        );
753        assert!(
754            message.contains("active_tasks=1"),
755            "bead_id={BEAD_ID} case=quiescence_error_task_count message={message}"
756        );
757        assert!(
758            message.contains("active_obligations=0"),
759            "bead_id={BEAD_ID} case=quiescence_error_obligation_count message={message}"
760        );
761    }
762
763    #[test]
764    fn test_no_detached_tasks() {
765        let tree = RegionTree::new();
766        // No regions exist — spawning a task without a valid region must fail.
767        let result = tree.register_task(Region::new(999));
768        assert!(
769            result.is_err(),
770            "bead_id={BEAD_ID} case=detached_task_rejected"
771        );
772    }
773
774    #[test]
775    fn test_complete_close_requires_closing_state() {
776        let mut tree = RegionTree::new();
777        let root = tree
778            .create_root(RegionKind::DbRoot, Cx::new())
779            .expect("root");
780        let child = tree
781            .create_child(root, RegionKind::WriteCoordinator, Cx::new())
782            .expect("child");
783
784        let err = tree
785            .complete_close(child)
786            .expect_err("must require begin_close");
787        assert!(
788            matches!(err, FrankenError::Internal(_)),
789            "bead_id={BEAD_ID} case=complete_close_requires_closing_state got {err:?}"
790        );
791        assert_eq!(
792            tree.state(child),
793            Some(RegionState::Open),
794            "bead_id={BEAD_ID} case=child_state_unchanged_when_close_rejected"
795        );
796    }
797
798    #[test]
799    fn test_database_close_awaits_quiescence() {
800        let mut tree = RegionTree::new();
801        let root = tree
802            .create_root(RegionKind::DbRoot, Cx::new())
803            .expect("root");
804
805        // Create service regions with active workers.
806        let wc = tree
807            .create_child(root, RegionKind::WriteCoordinator, Cx::new())
808            .expect("wc");
809        let gc = tree
810            .create_child(root, RegionKind::CheckpointGc, Cx::new())
811            .expect("gc");
812
813        let wc_task = tree.register_task(wc).expect("wc task");
814        let gc_task = tree.register_task(gc).expect("gc task");
815
816        let finalized = Arc::new(AtomicBool::new(false));
817        {
818            let flag = Arc::clone(&finalized);
819            tree.register_finalizer(root, move || {
820                flag.store(true, Ordering::Release);
821            })
822            .expect("root finalizer");
823        }
824
825        // Begin close of root (cascades to children).
826        tree.begin_close(root).expect("begin close root");
827        assert_eq!(tree.state(wc), Some(RegionState::Closing));
828        assert_eq!(tree.state(gc), Some(RegionState::Closing));
829
830        // Root is not quiescent yet (active child tasks).
831        assert!(
832            !tree.is_quiescent(root),
833            "bead_id={BEAD_ID} case=root_not_quiescent_with_active_children"
834        );
835
836        // Complete child tasks.
837        drop(wc_task);
838        assert!(
839            !tree.is_quiescent(root),
840            "bead_id={BEAD_ID} case=root_not_quiescent_gc_still_active"
841        );
842        drop(gc_task);
843
844        // Children are quiescent but not yet closed.
845        assert!(tree.is_quiescent(wc));
846        assert!(tree.is_quiescent(gc));
847        tree.complete_close(wc).expect("close wc");
848        tree.complete_close(gc).expect("close gc");
849
850        // Now root is quiescent.
851        assert!(
852            tree.is_quiescent(root),
853            "bead_id={BEAD_ID} case=root_quiescent_after_children_closed"
854        );
855        tree.complete_close(root).expect("close root");
856
857        assert!(
858            finalized.load(Ordering::Acquire),
859            "bead_id={BEAD_ID} case=root_finalizer_ran"
860        );
861        assert_eq!(tree.state(root), Some(RegionState::Closed));
862    }
863
864    #[test]
865    fn test_per_connection_region_child_of_root() {
866        let mut tree = RegionTree::new();
867        let root = tree
868            .create_root(RegionKind::DbRoot, Cx::new())
869            .expect("root");
870        let conn = tree
871            .create_child(root, RegionKind::PerConnection, Cx::new())
872            .expect("conn");
873
874        assert_eq!(
875            tree.parent(conn),
876            Some(Some(root)),
877            "bead_id={BEAD_ID} case=connection_is_child_of_root"
878        );
879        assert_eq!(tree.kind(conn), Some(RegionKind::PerConnection));
880
881        // Closing root cascades cancellation to connection region.
882        let conn_cx = tree.cx(conn).expect("conn cx");
883        tree.begin_close(root).expect("begin close root");
884        assert!(
885            conn_cx.is_cancel_requested(),
886            "bead_id={BEAD_ID} case=root_close_cancels_connection"
887        );
888        assert_eq!(tree.state(conn), Some(RegionState::Closing));
889
890        tree.complete_close(conn).expect("close conn");
891        tree.complete_close(root).expect("close root");
892    }
893
894    #[test]
895    #[allow(clippy::too_many_lines)]
896    fn test_e2e_structured_concurrency_shutdown() {
897        let mut tree = RegionTree::new();
898        let root = tree
899            .create_root(RegionKind::DbRoot, Cx::new())
900            .expect("root");
901
902        // Create normative service regions.
903        let wc = tree
904            .create_child(root, RegionKind::WriteCoordinator, Cx::new())
905            .expect("wc");
906        let ss = tree
907            .create_child(root, RegionKind::SymbolStore, Cx::new())
908            .expect("ss");
909        let repl = tree
910            .create_child(root, RegionKind::Replication, Cx::new())
911            .expect("repl");
912        let gc = tree
913            .create_child(root, RegionKind::CheckpointGc, Cx::new())
914            .expect("gc");
915        let obs = tree
916            .create_child(root, RegionKind::Observability, Cx::new())
917            .expect("obs");
918
919        // Create 3 connection regions with transaction children.
920        let conns: Vec<Region> = (0..3)
921            .map(|_| {
922                tree.create_child(root, RegionKind::PerConnection, Cx::new())
923                    .expect("conn")
924            })
925            .collect();
926
927        let mut txn_tasks = Vec::new();
928        for &conn in &conns {
929            let txn = tree
930                .create_child(conn, RegionKind::PerTransaction, Cx::new())
931                .expect("txn");
932            txn_tasks.push(tree.register_task(txn).expect("txn task"));
933        }
934
935        // Background workers in service regions.
936        let service_tasks = vec![
937            tree.register_task(wc).expect("wc task"),
938            tree.register_task(ss).expect("ss task"),
939            tree.register_task(repl).expect("repl task"),
940            tree.register_task(gc).expect("gc task"),
941            tree.register_task(obs).expect("obs task"),
942        ];
943
944        // Finalizers on root.
945        let finalized_count = Arc::new(AtomicUsize::new(0));
946        for _ in 0..3 {
947            let counter = Arc::clone(&finalized_count);
948            tree.register_finalizer(root, move || {
949                counter.fetch_add(1, Ordering::AcqRel);
950            })
951            .expect("root finalizer");
952        }
953
954        // Begin close of root (should cascade to all descendants).
955        tree.begin_close(root).expect("begin close root");
956
957        // All regions should be Closing.
958        assert_eq!(tree.state(root), Some(RegionState::Closing));
959        for &conn in &conns {
960            assert_eq!(tree.state(conn), Some(RegionState::Closing));
961        }
962
963        // Nothing is quiescent yet.
964        assert!(
965            !tree.is_quiescent(root),
966            "bead_id={BEAD_ID} case=e2e_root_not_quiescent_initially"
967        );
968
969        // Complete all tasks.
970        drop(txn_tasks);
971        drop(service_tasks);
972
973        // Close bottom-up: transactions → connections → services → root.
974        for &conn in &conns {
975            let txn_children = tree.children(conn).expect("conn children").to_vec();
976            for txn in txn_children {
977                tree.complete_close(txn).expect("close txn");
978            }
979        }
980        for &conn in &conns {
981            tree.complete_close(conn).expect("close conn");
982        }
983        for &svc in &[wc, ss, repl, gc, obs] {
984            tree.complete_close(svc).expect("close svc");
985        }
986
987        // Finally close root.
988        assert!(
989            tree.is_quiescent(root),
990            "bead_id={BEAD_ID} case=e2e_root_quiescent"
991        );
992        tree.complete_close(root).expect("close root");
993
994        assert_eq!(
995            finalized_count.load(Ordering::Acquire),
996            3,
997            "bead_id={BEAD_ID} case=e2e_all_finalizers_ran"
998        );
999        assert_eq!(
1000            tree.state(root),
1001            Some(RegionState::Closed),
1002            "bead_id={BEAD_ID} case=e2e_root_closed"
1003        );
1004
1005        // Verify zero orphan tasks.
1006        assert_eq!(tree.active_tasks(root), 0);
1007        for &conn in &conns {
1008            assert_eq!(tree.active_tasks(conn), 0);
1009        }
1010    }
1011
1012    #[test]
1013    fn test_close_and_drain_threaded() {
1014        use std::sync::Mutex;
1015        use std::thread;
1016        use std::time::Duration;
1017
1018        let tree = Arc::new(Mutex::new(RegionTree::new()));
1019        let root = {
1020            let mut t = tree.lock().unwrap_or_else(|e| e.into_inner());
1021            t.create_root(RegionKind::DbRoot, Cx::new()).expect("root")
1022        };
1023        let wc = {
1024            let mut t = tree.lock().unwrap_or_else(|e| e.into_inner());
1025            t.create_child(root, RegionKind::WriteCoordinator, Cx::new())
1026                .expect("wc")
1027        };
1028
1029        // Register tasks before spawning threads.
1030        let task1 = tree
1031            .lock()
1032            .unwrap_or_else(|e| e.into_inner())
1033            .register_task(wc)
1034            .expect("t1");
1035        let task2 = tree
1036            .lock()
1037            .unwrap_or_else(|e| e.into_inner())
1038            .register_task(wc)
1039            .expect("t2");
1040
1041        let completed = Arc::new(AtomicBool::new(false));
1042        let flag = Arc::clone(&completed);
1043
1044        // Spawn threads that hold tasks and complete after brief work.
1045        let t1 = thread::spawn(move || {
1046            thread::sleep(Duration::from_millis(20));
1047            drop(task1);
1048        });
1049        let t2 = thread::spawn(move || {
1050            thread::sleep(Duration::from_millis(30));
1051            drop(task2);
1052        });
1053
1054        // close_and_drain blocks until all tasks complete.
1055        {
1056            let mut t = tree.lock().unwrap_or_else(|e| e.into_inner());
1057            t.close_and_drain(root).expect("close_and_drain");
1058        }
1059        flag.store(true, Ordering::Release);
1060
1061        t1.join().expect("t1 join");
1062        t2.join().expect("t2 join");
1063
1064        assert!(
1065            completed.load(Ordering::Acquire),
1066            "bead_id={BEAD_ID} case=threaded_close_completed"
1067        );
1068        assert_eq!(
1069            tree.lock().unwrap_or_else(|e| e.into_inner()).state(root),
1070            Some(RegionState::Closed),
1071            "bead_id={BEAD_ID} case=threaded_root_closed"
1072        );
1073    }
1074}