mem-engine-foundations — the mem engine's foundations land (alkstore-mem, wave 6 task 1 of the ADR-024 chain): fourth workspace member with the manifest surface ADR-024 §1 pins — alkstore versioned path pin (ADR-017 §4.1) plus tokio restricted to the wasm-supported subset (sync + time; dev-deps rt/macros for the current-thread-flavor tests), no driver deps, no parking_lot (the description's prose named it, but the ADR + acceptance criterion pin the manifest tighter — short-held internal guards are std::sync::Mutex behind a state::Guard trait that folds poisoning away structurally, guards never held across awaits or user code, tokio mpsc the cross-await primitive). Five modules re-exported from lib.rs whose crate docs carry the ADR-016 posture statement (honest-ephemeral, full contract v1, wasm32-compile-clean identity, architecture sketch with the wave-6 construction line). MemStore::new() is the engine-native isolated constructor (ADR-024 §4's documented open-prose exception): fresh guarded state core (state.rs — empty named per-mechanism sections QueueState/StreamState/LockState/ScheduleState, shapes owned by the mechanism tasks), fresh wake bus, fresh clock per open; teardown is drop driving WakeBus::close() — no explicit close form. Wake substrate (wakes.rs): WakeBus::attach → WakeFeed (one unbounded mpsc feed per subscriber, detach-on-drop, attach under the registry guard so attach-after-commit no-replay is structural), fire_commit_wakes(channels) as the commit-ordered wake source's entry point (guard makes watermark bump + per-channel fanout one operation — per-subscriber FIFO by commit order, never coalesced), close empties senders so engine drop disconnects every feed; commit_watermark is a cfg(test) ordering observable only. Clock (clock.rs): one accessor Clock::unix_now over SystemTime, freeze/advance/unfreeze as the cfg(test) deterministic-seam; MemStore clock/wakes cfg(test) accessors. Validation (validation.rs) carries no new rule — pin module only (empty/whitespace InvalidName, prefix ReservedName, RESERVED_LISTENER_RECONNECTED rejected though unused here); mechanisms route core's validate_shared_name/local_name directly. Targeted allow(dead_code) on not-yet-wired scaffold surfaces so clippy -D warnings stays green pre-consumer (mechanism tasks own exhausting the allows); tokio time pinned unexercised (lock-TTL/scheduler consumers arrive with their tasks). Unit pins: 17 mem tests — two-instances-share-nothing (Arc pointer identity + behavioral: frozen clock isolation, cross-instance wake silence), state section independence/relock, wake routing (channel-scoped + foreign silence, per-notify never coalesced, watermark-ordered FIFO, no-replay, close disconnects all + post-close inert, dropped feed detaches), engine-drop closes feeds, clock determinism, validation pin row. Task file: status completed, Notes (decisions of record: parking_lot rejection, empty sections, substrate API shape, no-close, scaffold allow posture, unexercised time feature) + Summary filled. Verified: cargo build; workspace cargo test green (pg 124/25/9, sqlite 191/25, core 25, suite 3, mem 17); clippy --all-targets -D warnings clean; cargo fmt --check clean; cargo check --target wasm32-unknown-unknown -p alkstore-mem clean — the wave-6 wasm gate starts green from this task onward

This commit is contained in:
glm-5.3-flash committed 2026-10-10 11:27:52 +00:00
1 parent 6f9cfb5778
commit 6632dcce1f
10 files changed
+938 -8

No files matched your search

+334
View File
@@ -0,0 +1,334 @@
//! 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 now — streams and
//! queue wakes arrive with their tasks) 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 &registry.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"
);
}
}