RFC 016 Chunk 3: parentage tree view
tree() / tree_from() fold a Chunk-1 snapshot into a forest by grouping each actor under its parent pid — one O(n) pass, no new reads. tree_from is public so a held (or synthetic) snapshot can be folded without a second scan. - D8: actors parented at ROOT_PID are genuine roots; an actor whose recorded parent is absent from the snapshot is re-rooted under the sentinel and flagged orphaned, so the forest stays total. take()-on- place doubles as a guard against re-entering a node. - D9: the edge is parent/spawned-by, documented as not necessarily supervision.
This commit is contained in:
@@ -29,6 +29,7 @@ use crate::scheduler::with_runtime;
|
||||
use crate::slot_state::{
|
||||
word_gen, word_state, ST_DONE, ST_PARKED, ST_QUEUED, ST_RUNNING, ST_RUNNING_NOTIFIED,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Snapshot wire-format version (DECISION D1). [`RuntimeSnapshot`] is treated as
|
||||
/// a stable type from day one: it becomes the observer protocol (Chunk 4) and
|
||||
@@ -178,3 +179,90 @@ fn read_slot(slot: &Slot, idx: u32, mail: Option<&MailboxInfo>) -> Option<ActorI
|
||||
mailbox_depth,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chunk 3 — tree view (pure derivation over a Chunk-1 snapshot)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One node in the parentage forest. `children` are the actors whose recorded
|
||||
/// parent edge points at this node's pid.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TreeNode {
|
||||
pub info: ActorInfo,
|
||||
/// The actor's recorded parent was absent from the snapshot (already
|
||||
/// Done/Vacant, or itself a tombstone), so it was re-rooted under the forest
|
||||
/// sentinel rather than dropped — the tree stays total (DECISION D8).
|
||||
pub orphaned: bool,
|
||||
pub children: Vec<TreeNode>,
|
||||
}
|
||||
|
||||
/// The parentage forest. Roots are actors parented at `ROOT_PID` (genuine
|
||||
/// roots) plus re-rooted orphans. The edge is *spawned-by / parent*, not
|
||||
/// necessarily supervision (DECISION D9) — see [`ActorInfo::supervisor`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RuntimeTree {
|
||||
pub format_version: u16,
|
||||
pub roots: Vec<TreeNode>,
|
||||
}
|
||||
|
||||
/// Take a live [`snapshot`] and fold it into the parentage forest.
|
||||
pub fn tree() -> RuntimeTree {
|
||||
tree_from(snapshot())
|
||||
}
|
||||
|
||||
/// Fold an existing snapshot into a forest by grouping each actor under its
|
||||
/// parent pid — a single O(n) pass, no new reads. Exposed separately so a
|
||||
/// consumer that already holds a snapshot (or a synthetic one, in tests) can
|
||||
/// derive the tree without a second scan.
|
||||
pub fn tree_from(snap: RuntimeSnapshot) -> RuntimeTree {
|
||||
let RuntimeSnapshot { format_version, actors } = snap;
|
||||
|
||||
let mut index_of: HashMap<Pid, usize> = HashMap::with_capacity(actors.len());
|
||||
for (i, a) in actors.iter().enumerate() {
|
||||
index_of.insert(a.pid, i);
|
||||
}
|
||||
|
||||
// Group children under their present parent; everything else is a root.
|
||||
// Scan order is preserved within each parent's child list.
|
||||
let mut children_of: HashMap<Pid, Vec<usize>> = HashMap::new();
|
||||
let mut roots: Vec<usize> = Vec::new();
|
||||
let mut orphaned = vec![false; actors.len()];
|
||||
for (i, a) in actors.iter().enumerate() {
|
||||
let parent = a.supervisor;
|
||||
if parent != ROOT_PID && index_of.contains_key(&parent) {
|
||||
children_of.entry(parent).or_default().push(i);
|
||||
} else {
|
||||
// Parent is the forest sentinel (genuine root) or absent from the
|
||||
// snapshot (orphan, D8) — either way a root of the forest.
|
||||
orphaned[i] = parent != ROOT_PID;
|
||||
roots.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// `take()` each actor as it is placed, which also guards against a
|
||||
// (constructionally impossible) parentage cycle re-entering a node.
|
||||
let mut slots: Vec<Option<ActorInfo>> = actors.into_iter().map(Some).collect();
|
||||
let root_nodes = roots
|
||||
.into_iter()
|
||||
.filter_map(|i| build_node(i, &children_of, &orphaned, &mut slots))
|
||||
.collect();
|
||||
RuntimeTree { format_version, roots: root_nodes }
|
||||
}
|
||||
|
||||
fn build_node(
|
||||
i: usize,
|
||||
children_of: &HashMap<Pid, Vec<usize>>,
|
||||
orphaned: &[bool],
|
||||
slots: &mut [Option<ActorInfo>],
|
||||
) -> Option<TreeNode> {
|
||||
let info = slots[i].take()?; // already placed → cycle guard / no double-attach
|
||||
let children = children_of
|
||||
.get(&info.pid)
|
||||
.map(|kids| {
|
||||
kids.iter()
|
||||
.filter_map(|&c| build_node(c, children_of, orphaned, slots))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Some(TreeNode { info, orphaned: orphaned[i], children })
|
||||
}
|
||||
|
||||
+2
-1
@@ -56,7 +56,8 @@ pub use gen_server::{
|
||||
NamedServerBuilder, ServerBuilder, ServerCtx, ServerName, ServerRef, TimerHandle, Watcher,
|
||||
};
|
||||
pub use introspect::{
|
||||
actor_info, snapshot, ActorInfo, ActorState, RuntimeSnapshot, SNAPSHOT_FORMAT_VERSION,
|
||||
actor_info, snapshot, tree, tree_from, ActorInfo, ActorState, RuntimeSnapshot, RuntimeTree,
|
||||
TreeNode, SNAPSHOT_FORMAT_VERSION,
|
||||
};
|
||||
pub use link::{link, trap_exit, unlink, ExitSignal};
|
||||
pub use monitor::{demonitor, monitor, Down, DownReason, Monitor, MonitorId};
|
||||
|
||||
+78
-2
@@ -4,8 +4,8 @@
|
||||
//! instead of sleeping-and-hoping.
|
||||
|
||||
use smarm::{
|
||||
actor_info, channel, monitor, register, run, self_pid, send, snapshot, spawn, ActorState, Name,
|
||||
Pid, SNAPSHOT_FORMAT_VERSION,
|
||||
actor_info, channel, monitor, register, run, self_pid, send, snapshot, spawn, tree, tree_from,
|
||||
ActorInfo, ActorState, Name, Pid, RuntimeSnapshot, SNAPSHOT_FORMAT_VERSION,
|
||||
};
|
||||
|
||||
const SVC: Name<u64> = Name::new("svc");
|
||||
@@ -182,3 +182,79 @@ fn done_actor_is_a_tombstone() {
|
||||
h.join().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_places_child_under_its_spawner() {
|
||||
run(|| {
|
||||
let (ready_tx, ready_rx) = channel::<()>();
|
||||
let (gate_tx, gate_rx) = channel::<()>();
|
||||
let h = spawn(move || {
|
||||
ready_tx.send(()).unwrap();
|
||||
gate_rx.recv().unwrap();
|
||||
});
|
||||
ready_rx.recv().unwrap();
|
||||
|
||||
let me = self_pid();
|
||||
let t = tree();
|
||||
assert_eq!(t.format_version, SNAPSHOT_FORMAT_VERSION);
|
||||
|
||||
// The root is parented at the forest sentinel, so it's a genuine root,
|
||||
// and the worker it spawned hangs beneath it.
|
||||
let root = t.roots.iter().find(|n| n.info.pid == me).expect("root in forest");
|
||||
assert!(!root.orphaned);
|
||||
assert!(
|
||||
root.children.iter().any(|c| c.info.pid == h.pid()),
|
||||
"spawned worker should be a child of its spawner"
|
||||
);
|
||||
|
||||
gate_tx.send(()).unwrap();
|
||||
h.join().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
/// D8 re-rooting and nesting, exercised on a synthetic snapshot via the public
|
||||
/// `tree_from` — the live lifecycle race (parent reclaimed while child lives)
|
||||
/// is exactly what's awkward to stage deterministically, which is why the fold
|
||||
/// is testable in isolation.
|
||||
#[test]
|
||||
fn tree_from_nests_children_and_reroots_orphans() {
|
||||
let root_sentinel = Pid::new(u32::MAX, u32::MAX);
|
||||
let root_pid = Pid::new(0, 1);
|
||||
let child = Pid::new(1, 1);
|
||||
let orphan = Pid::new(2, 1);
|
||||
let absent_parent = Pid::new(99, 1);
|
||||
|
||||
let mk = |pid: Pid, supervisor: Pid| ActorInfo {
|
||||
pid,
|
||||
names: Vec::new(),
|
||||
state: ActorState::Running,
|
||||
supervisor,
|
||||
trap_exit: false,
|
||||
monitors: 0,
|
||||
links: 0,
|
||||
joiners: 0,
|
||||
mailbox_depth: 0,
|
||||
};
|
||||
|
||||
let snap = RuntimeSnapshot {
|
||||
format_version: SNAPSHOT_FORMAT_VERSION,
|
||||
actors: vec![
|
||||
mk(root_pid, root_sentinel),
|
||||
mk(child, root_pid),
|
||||
mk(orphan, absent_parent),
|
||||
],
|
||||
};
|
||||
|
||||
let t = tree_from(snap);
|
||||
assert_eq!(t.roots.len(), 2);
|
||||
|
||||
let root = t.roots.iter().find(|n| n.info.pid == root_pid).expect("root present");
|
||||
assert!(!root.orphaned);
|
||||
assert_eq!(root.children.len(), 1);
|
||||
assert_eq!(root.children[0].info.pid, child);
|
||||
assert!(!root.children[0].orphaned);
|
||||
|
||||
let o = t.roots.iter().find(|n| n.info.pid == orphan).expect("orphan re-rooted");
|
||||
assert!(o.orphaned, "an actor whose parent is absent must be flagged orphaned");
|
||||
assert!(o.children.is_empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user