The born-pinned gate audited the mem engine against the contract and ADR-024: the conformance read clean in mechanism substance (queue state machine, scheduler collapse, outbox, streams, locks, clock, validation, tx overlay, wake substrate — incl. a full guard-order and panic-hygiene audit); the suite's third column read row-by-row and 25/25 green under the per-test current-thread flavor (24 engine-shared rows + mem's payload_too_large non-occurrence arm + the factory-shape test); three-way arithmetic equivalence green on all columns; the wasm32 acceptance items verified (compile-clean, tokio sync+time, no blocking, flavor enforced); the rider's two arms verified (Finding 1 discharged; Findings 2-3 stay parked for wave 7); workspace gates, taskgraph validate green. Three edge-domain fixes landed inline: the stream cursor read's i64::MAX overflow (Bound::Excluded), trim_to's i64::MAX horizon endpoint (Bound::Included), and the scheduler register path now rejecting a parseable interval whose first boundary overflows with InvalidSpec — the one engine-vs-engine divergence the read found, aligned into the class-1 window — plus a pinning test and scaffold- residue doc/comment cleanups (clock allows, tx per-stream wake comment, wakes/stream module docs). Recorded findings with dispositions in the task's Notes for the wave-7 pre-release pass: the multi-thread publish-to-wake ordering tolerance, the heartbeat/ save_offset/EventReceiver-try_recv ADR-023 §2 classification gaps, and the mem arithmetic being a suite-pinned transplant. Verified: cargo build; cargo test --workspace green (mem 101 lib + 25 suite, sqlite 191+25, pg 124+25+9 server-less, core 25, harness 3); clippy --all-targets -D warnings clean; fmt clean; taskgraph validate (61 tasks). Gate record: tasks/review-wave-6.md (Notes + Summary), implementation.md wave-6 row + review-rounds entry.
336 lines
12 KiB
Rust
336 lines
12 KiB
Rust
//! The wake-routing substrate: in-process channels carrying wakes at
|
|
//! the commit boundary (engine-mem.md §Architecture).
|
|
//!
|
|
//! There is no batching boundary — wakes deliver at the commit
|
|
//! boundary directly to each open receiver, per-subscriber FIFO by
|
|
//! commit order; coalescing is not applicable. The substrate realizes
|
|
//! the shape structurally:
|
|
//!
|
|
//! - **Commit-ordered source**: a mechanism (notify, stream publishes,
|
|
//! the tx commit-merge's staged notifies and publishes) fires its
|
|
//! commit's wakes
|
|
//! through [`WakeBus::fire_commit_wakes`], under one guard: the
|
|
//! commit sequence bumps and the per-channel fanout completes as a
|
|
//! single operation, so every subscriber's receipt order is exactly
|
|
//! commit order. Two concurrent commits serialize on the guard; a
|
|
//! subscriber's wake stream is FIFO by commit order.
|
|
//! - **Per-channel attach**: [`WakeBus::attach`] registers under the
|
|
//! same guard and returns an unbounded [`tokio::sync::mpsc`] feed —
|
|
//! attach never blocks, and a receiver attached after a commit never
|
|
//! sees it (no replay; the attach either happens before that commit's
|
|
//! fanout or after it — the guard admits no mid-fanout attach).
|
|
//! - **Subscriber registry**: unbounded senders live only in the bus,
|
|
//! so the sole close path is the engine's drop. [`WakeBus::close`]
|
|
//! empties the registry under the same guard — every live feed's
|
|
//! sender drops and each receiver drains to `Disconnected`; the
|
|
//! consumer-visible `recv() -> None` terminal arm arrives with
|
|
//! `mem-engine-notify-listen`'s [`alkstore::WakeReceiver`] bridge
|
|
//! over this substrate. A dropped feed detaches itself
|
|
//! ([`WakeFeed`]'s `Drop`).
|
|
//!
|
|
//! The `Wake` value crosses exactly as the contract pins it — channel
|
|
//! only, never payloads or ids (ADR-008 §3). The fanout is per-notify:
|
|
//! one commit's notify on a channel is one wake per subscribed feed,
|
|
//! never coalesced (no batching boundary exists to coalesce across).
|
|
|
|
// The substrate routes every wave-6 mechanism's commit-boundary
|
|
// wakes; until the mechanism tasks wire it, the foundations-only
|
|
// build sees the attach/fire surface as dead.
|
|
#![allow(dead_code)]
|
|
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
|
|
use alkstore::Wake;
|
|
|
|
use crate::state::Guard;
|
|
|
|
/// A subscriber's registry row: its id (the detach key), the channel
|
|
/// it attached on (exact-match deliverance), and its feed's sender.
|
|
/// Senders live only here — a subscriber holds no bus reference of its
|
|
/// own beyond its [`WakeFeed`] (which carries the bus Arc for
|
|
/// detach-on-drop, never for delivery), so the engine's close empties
|
|
/// the senders and the feeds disconnect regardless of who still holds
|
|
/// a feed.
|
|
#[derive(Debug)]
|
|
struct Subscription {
|
|
id: u64,
|
|
channel: String,
|
|
tx: tokio::sync::mpsc::UnboundedSender<Wake>,
|
|
}
|
|
|
|
/// The internal guarded registry: next-subscriber id, next commit
|
|
/// sequence, and the live subscriptions in first-attached order.
|
|
#[derive(Debug, Default)]
|
|
struct Registry {
|
|
next_subscriber_id: u64,
|
|
next_commit_seq: u64,
|
|
subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
/// The in-process wake bus: the commit-ordered wake source's entry
|
|
/// point ([`WakeBus::fire_commit_wakes`]) and the per-channel receiver
|
|
/// attach ([`WakeBus::attach`]) over one guarded registry.
|
|
pub(crate) struct WakeBus {
|
|
registry: Mutex<Registry>,
|
|
closed: std::sync::atomic::AtomicBool,
|
|
}
|
|
|
|
impl Default for WakeBus {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for WakeBus {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("WakeBus").finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl WakeBus {
|
|
#[allow(dead_code)]
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
registry: Mutex::new(Registry::default()),
|
|
closed: std::sync::atomic::AtomicBool::new(false),
|
|
}
|
|
}
|
|
|
|
/// Attach a feed for `channel` — starts from "now": the attach
|
|
/// lands under the registry guard, so commits that fired before it
|
|
/// are never replayed into it. Returns the feed plus its detach
|
|
/// handle (drop the [`WakeFeed`] to detach).
|
|
pub(crate) fn attach(self: &Arc<Self>, channel: &str) -> WakeFeed {
|
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
|
let mut registry = self.registry.guard();
|
|
if self.closed.load(std::sync::atomic::Ordering::Acquire) {
|
|
return WakeFeed {
|
|
id: 0,
|
|
bus: Arc::clone(self),
|
|
rx: dead_feed_receiver(),
|
|
};
|
|
}
|
|
registry.next_subscriber_id += 1;
|
|
let id = registry.next_subscriber_id;
|
|
registry.subscriptions.push(Subscription {
|
|
id,
|
|
channel: channel.to_string(),
|
|
tx,
|
|
});
|
|
WakeFeed {
|
|
id,
|
|
bus: Arc::clone(self),
|
|
rx,
|
|
}
|
|
}
|
|
|
|
/// Fire the commit boundary's wakes — the mechanism's wake source:
|
|
/// under one guard, the commit sequence bumps once and every feed
|
|
/// attached on a fired channel receives one [`Wake`] per commit-notify,
|
|
/// in commit order. A channel with no subscribers delivers nothing.
|
|
/// A closed bus is inert.
|
|
pub(crate) fn fire_commit_wakes(&self, channels: impl IntoIterator<Item = String>) {
|
|
if self.closed.load(std::sync::atomic::Ordering::Acquire) {
|
|
return;
|
|
}
|
|
let mut registry = self.registry.guard();
|
|
registry.next_commit_seq += 1;
|
|
let fired: Vec<String> = channels.into_iter().collect();
|
|
for channel in &fired {
|
|
for subscription in ®istry.subscriptions {
|
|
if subscription.channel == *channel {
|
|
let wake = Wake::new(channel.clone());
|
|
let _ = subscription.tx.send(wake);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The commit watermark: how many commit boundaries have fired
|
|
/// through this bus. The substrate's ordering observable — the
|
|
/// consumer contract never surfaces it (wakes carry the channel
|
|
/// only); unit tests pin FIFO-by-commit-order against it.
|
|
#[cfg(test)]
|
|
pub(crate) fn commit_watermark(&self) -> u64 {
|
|
self.registry.guard().next_commit_seq
|
|
}
|
|
|
|
/// Close the bus — the engine's drop path. Empties the registry
|
|
/// under one guard: every feed's sender drops, receivers drain to
|
|
/// disconnected, and attach/fire after it are no-ops. Idempotent.
|
|
pub(crate) fn close(&self) {
|
|
self.closed
|
|
.store(true, std::sync::atomic::Ordering::Release);
|
|
let mut registry = self.registry.guard();
|
|
registry.subscriptions.clear();
|
|
}
|
|
|
|
fn detach(&self, id: u64) {
|
|
let mut registry = self.registry.guard();
|
|
registry.subscriptions.retain(|sub| sub.id != id);
|
|
}
|
|
}
|
|
|
|
/// A feed whose sender never existed — the attach-on-closed shape (the
|
|
/// engine is gone; the feed reads as closed immediately, the same
|
|
/// terminal arm an engine drop produces).
|
|
fn dead_feed_receiver() -> tokio::sync::mpsc::UnboundedReceiver<Wake> {
|
|
let (_tx, rx) = tokio::sync::mpsc::unbounded_channel::<Wake>();
|
|
rx
|
|
}
|
|
|
|
/// A receiver's handle into the substrate: the raw unbounded feed of
|
|
/// [`Wake`] values (per-subscriber FIFO by commit order — see the
|
|
/// module docs) plus its own-detach-on-drop. This is the substrate
|
|
/// primitive; the consumer-visible `WakeReceiver` bridge (the
|
|
/// `recv`/`try_recv`/`recv_timeout` forms and the terminal `None`
|
|
/// arm) is `mem-engine-notify-listen`'s surface over it.
|
|
pub(crate) struct WakeFeed {
|
|
id: u64,
|
|
bus: Arc<WakeBus>,
|
|
rx: tokio::sync::mpsc::UnboundedReceiver<Wake>,
|
|
}
|
|
|
|
impl std::fmt::Debug for WakeFeed {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("WakeFeed")
|
|
.field("id", &self.id)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl WakeFeed {
|
|
pub(crate) fn rx(&mut self) -> &mut tokio::sync::mpsc::UnboundedReceiver<Wake> {
|
|
&mut self.rx
|
|
}
|
|
}
|
|
|
|
impl Drop for WakeFeed {
|
|
fn drop(&mut self) {
|
|
if self.id != 0 {
|
|
self.bus.detach(self.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
use tokio::sync::mpsc::error::TryRecvError;
|
|
|
|
fn delivered(feed: &mut WakeFeed) -> Vec<Wake> {
|
|
let mut wakes = Vec::new();
|
|
loop {
|
|
match feed.rx().try_recv() {
|
|
Ok(wake) => wakes.push(wake),
|
|
Err(TryRecvError::Empty) => break,
|
|
Err(TryRecvError::Disconnected) => {
|
|
panic!("the feed disconnected unexpectedly")
|
|
}
|
|
}
|
|
}
|
|
wakes
|
|
}
|
|
|
|
#[test]
|
|
fn delivery_is_channel_scoped_and_never_coalesced() {
|
|
let bus = Arc::new(WakeBus::new());
|
|
let mut a = bus.attach("orders");
|
|
let mut b = bus.attach("orders");
|
|
let mut foreign = bus.attach("payments");
|
|
bus.fire_commit_wakes(vec!["orders".into(), "orders".into(), "payments".into()]);
|
|
let a_wakes = delivered(&mut a);
|
|
let b_wakes = delivered(&mut b);
|
|
let foreign_wakes = delivered(&mut foreign);
|
|
assert_eq!(
|
|
a_wakes, b_wakes,
|
|
"subscribers on the same channel receive the same wakes"
|
|
);
|
|
assert!(
|
|
a_wakes.iter().all(|w| w.channel == "orders"),
|
|
"each wake carries its channel"
|
|
);
|
|
assert_eq!(
|
|
foreign_wakes[0].channel, "payments",
|
|
"a foreign-channel subscriber receives only its own channel"
|
|
);
|
|
assert_eq!(
|
|
a_wakes.len(),
|
|
2,
|
|
"per-notify delivery — one wake per commit, never coalesced"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn per_subscriber_fifo_by_commit_order() {
|
|
let bus = Arc::new(WakeBus::new());
|
|
let mut feed = bus.attach("orders");
|
|
for seq in 0..8 {
|
|
assert_eq!(
|
|
bus.commit_watermark(),
|
|
seq,
|
|
"the watermark advances one per fired commit boundary"
|
|
);
|
|
bus.fire_commit_wakes(vec![format!("orders-{seq}"), "orders".into()]);
|
|
}
|
|
assert_eq!(bus.commit_watermark(), 8);
|
|
let wakes = delivered(&mut feed);
|
|
assert_eq!(
|
|
wakes,
|
|
vec![Wake::new("orders".into()); 8],
|
|
"each commit's wake delivered, in commit order"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn attach_after_commit_sees_nothing_before_it() {
|
|
let bus = Arc::new(WakeBus::new());
|
|
bus.fire_commit_wakes(vec!["orders".into()]);
|
|
let mut late = bus.attach("orders");
|
|
assert!(
|
|
late.rx().try_recv().is_err(),
|
|
"a listener attached after a commit never sees it (no replay)"
|
|
);
|
|
bus.fire_commit_wakes(vec!["orders".into()]);
|
|
assert_eq!(
|
|
delivered(&mut late),
|
|
vec![Wake::new("orders".into())],
|
|
"post-attach commits deliver"
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn close_disconnects_every_live_feed() {
|
|
let bus = Arc::new(WakeBus::new());
|
|
let mut a = bus.attach("orders");
|
|
let mut b = bus.attach("payments");
|
|
bus.close();
|
|
bus.fire_commit_wakes(vec!["orders".into()]);
|
|
let mut late = bus.attach("orders");
|
|
for feed in [&mut a, &mut b, &mut late] {
|
|
assert!(
|
|
feed.rx().recv().await.is_none(),
|
|
"engine close is the terminal None arm for every open receiver"
|
|
);
|
|
}
|
|
assert_eq!(bus.commit_watermark(), 0, "a fire on a closed bus is inert");
|
|
}
|
|
|
|
#[test]
|
|
fn dropped_feed_detaches() {
|
|
let bus = Arc::new(WakeBus::new());
|
|
let feed = bus.attach("orders");
|
|
let mut kept = bus.attach("orders");
|
|
std::mem::drop(feed);
|
|
bus.fire_commit_wakes(vec!["orders".into()]);
|
|
let wakes = delivered(&mut kept);
|
|
assert_eq!(
|
|
wakes.len(),
|
|
1,
|
|
"the detached feed received nothing, the live one the wake"
|
|
);
|
|
}
|
|
}
|