async_nodes/merge.rs
1//! Fork/join branch merging.
2//!
3//! BSP supersteps run frontier nodes concurrently, so each frontier task executes against
4//! its own clone of the consumer state (Rust's aliasing rules make a shared `&mut S` into
5//! N concurrent futures impossible — the same reason the reference consumer passes state
6//! by value per branch). When the superstep barrier folds the branch states back into the
7//! trunk, a [`Merger`] decides *how*:
8//!
9//! - A join node names a merger (consumer-registered, like named conditions), and the
10//! engine folds branch states into the trunk **in frontier declaration order** — never
11//! in task completion order, which would make runs irreproducible.
12//! - Without a named merger the fold is last-writer-wins (still declaration order, still
13//! deterministic), and validation emits a warning — silent last-wins is how subtle state
14//! loss hides, and the warning is the point.
15//!
16//! Because every branch clones the trunk *at its superstep start*, writes folded at earlier
17//! barriers are visible to branches that start later; only same-superstep siblings are
18//! isolated from each other, and the merger resolves exactly those sibling conflicts.
19
20use std::{collections::HashMap, sync::Arc};
21
22use async_trait::async_trait;
23
24use super::error::BspError;
25
26/// How a branched state rejoins the trunk.
27///
28/// Implementations are consumer-owned: the engine knows nothing about what `S` fields mean.
29/// A typical merger replays a branch's delta into the trunk (e.g. folding a child store's
30/// integer deltas and rolling up metrics, the way the reference consumer's parallel tool
31/// map-reduce does).
32#[async_trait]
33pub trait Merger<S>: Send + Sync {
34 /// Folds one finished branch state back into the trunk.
35 fn merge(&self, trunk: &mut S, branch: S);
36
37 /// Finalizes a barrier fold: called exactly once after the last `merge` of a
38 /// multi-node superstep barrier (never called for single-node frontiers, which
39 /// mutate the trunk directly and never fold).
40 ///
41 /// This is the hook for *all-at-once* merge semantics: `merge` can accumulate the
42 /// branch states (interior mutability) and `finish` collapses them into the trunk in
43 /// one shot — e.g. the reference consumer's reducer barriers fold a whole
44 /// `Vec<GraphState>` through one async, fallible reduction instead of pairwise folds.
45 ///
46 /// It is `async` because barrier finalization may itself be a computation (the
47 /// reference consumer's reducers call LLMs), and fallible so a failed collapse
48 /// aborts the run loudly instead of silently continuing with an unreduced trunk.
49 /// The default is a no-op `Ok(())`, which is what pairwise `merge`-only mergers want.
50 async fn finish(&self, _trunk: &mut S) -> Result<(), BspError> {
51 Ok(())
52 }
53}
54
55/// Any `Fn(&mut S, S)` closure is a merger.
56#[async_trait]
57impl<S, F> Merger<S> for F
58where
59 F: Fn(&mut S, S) + Send + Sync,
60{
61 fn merge(&self, trunk: &mut S, branch: S) {
62 self(trunk, branch);
63 }
64}
65
66/// Map of merger name -> merger, carried by the graph.
67pub type MergerMap<S> = HashMap<String, Arc<dyn Merger<S>>>;
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn test_closure_is_a_merger() {
75 let merger = |trunk: &mut Vec<String>, branch: Vec<String>| {
76 for item in branch {
77 if !trunk.contains(&item) {
78 trunk.push(item);
79 }
80 }
81 };
82 let mut trunk = vec!["a".to_string()];
83 Merger::merge(&merger, &mut trunk, vec!["a".to_string(), "b".to_string()]);
84 assert_eq!(trunk, vec!["a".to_string(), "b".to_string()]);
85 }
86
87 #[test]
88 fn test_fold_declaration_order_is_deterministic() {
89 // Simulates the engine's barrier fold: branches folded in declaration order.
90 let append = |trunk: &mut Vec<&'static str>, mut branch: Vec<&'static str>| {
91 trunk.append(&mut branch);
92 };
93 let branches = [vec!["a"], vec!["b"], vec!["c"]];
94 let mut trunk = Vec::new();
95 for branch in branches {
96 Merger::merge(&append, &mut trunk, branch);
97 }
98 assert_eq!(trunk, vec!["a", "b", "c"]);
99 }
100}