Files
alkstore/alkstore-mem/src/store.rs
T
glm-5.3-flash 1c3e96802f mem-engine-seam-tx — the wave's load-bearing seam lands: the complete Store/TxHandle trait surfaces over the guarded state core, with the tx overlay as the discipline centerpiece. Wider than the sqlite/pg seam twins by design (those rode pre-ported substrate ops and left mechanism methods stubbed; mem has no substrate, and the task's acceptance pins the full trait surfaces implemented over guarded state — a Rust trait impl is all-or-nothing per method): the seam task therefore realizes the mechanisms' committed-state effect layer and every mechanism handle itself, and the wave-6 mechanism tasks now own their acceptance-criteria unit pins against this foundation. Store surface: notify (codec-typed encode as the entry check only — the wake delivers the channel alone; PayloadTooLarge never produced, the ADR-016 §5 non-occurrence arm pinned at 2 MiB), listen (WakeReceiver bridge with the pinned recv forms; attach starts from now, one close path at engine drop), stream/queue/outbox/try_lock (validated constructors → boxed core-owned handle traits: the ADR-010 queue state machine with claims/reclaim-eats-attempt/uniform validity predicate/dead-letter/sweep + mem's third-owner equal-jitter curve — std-only RandomState hash jitter, no rand; ADR-015 log+offsets+trim+wake-driven subscribe; ADR-008 §7 TTL registry with renew-past-expiry refused; ADR-014 outbox helper with run_once ack/retry), schedule/unschedule/run_schedules (ADR-009 collapse: @every-only third-owner parser + register-table with pre-parsed interval and resolved stamps; the 64-cap bounded catch-up + grid-aligned skip-forward tick fired-and-advanced under one guarded op (schedules→queues lock order); leadership through the reserved __alkstore_scheduler lock with runner-unique owner, renew-before-tick, keep-awake sleep renewing on cadence, LeadershipLost / clean-stop-Ok returns). Tx overlay: begin_tx allocates the handle-private overlay; all eleven *_tx stage with entry-point validation before any effect, Codec-typed encode, one clock read, ADR-020 §3 stamp resolution (plain 300/3/5/none, outbox 60/5/5, the per-job max_attempts override via resolution.rs's stamps_with_override); tx reads merge overlay over committed (read-your-own-writes: queue-scoped + dead-visible get_job_tx, monotone staged saves, offset-ASC merged reads, extent guards clamping empty); commit consumes the handle, merges under one guarded op (queues→streams, the canonical lock order documented in state.rs), and fires the overlay's pending wakes AFTER the merge — wake-at-commit structural from birth (the pg-fix-tx-wake failure shape cannot exist here); drop (explicit, or through an unwinding panic — the overlay is plain local data, nothing runs on discard) = rollback with no ghosts, trivially. Stage-time id/offset allocation (the pg-shaped story — rolled-back allocations gap out; claims order id ASC, offsets immutable/never renumbered). Stream subscribers ride a reserved-prefix engine-internal wake channel (__alkstore_stream_wake:{stream}) unreachable by consumer entry points, attached before the stored-offset read; notify's transport stores nothing. 89 mem unit tests: the overlay discipline rows the task pins, the numeric-domain rows (extents/durations/boundaries/grammar/validation), and the mechanism smoke pins (the defect-class catch: ack/ack_batch originally deleted before checking the predicate — fixed; a refusal must leave the row exactly as it was). Manifest: serde_json = 1 joins (the trait signatures cross Value; both engine twins carry it; not a driver dep — wasm32 gate stays green). Task file: status completed, Notes (the whole-engine scope disposition + 11 decision-of-record entries incl. lock-renewal delta vs the sqlite substrate, ScheduleRow shape, runner lease mechanics, substrate predicate parity) + Summary filled. Verified: cargo build; workspace cargo test green (core 25, suite harness 3, mem 89, SQLite 191 lib + 25 suite, pg 124 lib + 25 suite + 9 schema); clippy --all-targets -D warnings clean; cargo fmt --check clean; cargo check --target wasm32-unknown-unknown -p alkstore-mem clean
2026-10-10 11:55:39 +00:00

292 lines
9.8 KiB
Rust

//! The mem engine's store: `MemStore::new`, the engine-native
//! constructor (ADR-024 §4 — the one documented exception to the
//! open prose), and the guarded state it owns — plus the `Store` trait
//! surface this crate's whole point rides on.
//!
//! Isolation per open: each `new()` yields a fully isolated instance
//! sharing nothing — a fresh `State` (the guarded per-mechanism core),
//! a fresh `WakeBus` (the wake-routing substrate), a fresh `Clock` —
//! exactly the suite factory's isolation contract; two instances never
//! contend and never observe each other. There is no connection, no
//! file, no pool; teardown is drop — the engine-wide `Drop` runs the
//! close path (the wake bus empties, every open feed disconnects), and
//! the state is gone with the process's references, the honest-
//! ephemeral posture (ADR-016 carriers, ADR-024 §2: the dependency on
//! `alkstore-mem` *is* the ephemerality declaration).
//!
//! The `Store` surface wires the mechanism handles (core-owned trait
//! objects — engine types never appear in consumer signatures): `queue`
//! carries its `QueueOpts` stamps (queues are names, not config
//! owners), `stream`/`outbox` carry validated names, `try_lock` rides
//! the TTL registry, `schedule`/`unschedule`/`run_schedules` the
//! collapsed scheduler, `notify`/`listen` the wake substrate, and
//! `begin_tx` allocates the tx overlay (`crate::tx` — the load-bearing
//! seam; `with_tx` is the trait's provided composition).
use std::sync::Arc;
use alkstore::{BoxedFuture, Result, StopToken, Store, TxHandle};
use alkstore::{QueueOpts, ScheduleOpts};
use serde_json::Value;
use crate::clock::Clock;
use crate::queue::MemQueueHandle;
use crate::scheduler;
use crate::state::State;
use crate::stream::MemStreamHandle;
use crate::tx::MemTxHandle;
use crate::wakes::WakeBus;
/// The mem engine's store handle. Consumers hold
/// [`alkstore::Store`] — the trait is the contract surface, never the
/// engine type; `MemStore::new` is the engine-native constructor.
pub struct MemStore {
pub(crate) state: Arc<State>,
pub(crate) wakes: Arc<WakeBus>,
pub(crate) clock: Arc<Clock>,
}
impl Default for MemStore {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for MemStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemStore").finish_non_exhaustive()
}
}
impl MemStore {
/// Construct a fully isolated mem engine instance. Engine-native:
/// no connection string, no file path (ADR-024 §4's documented
/// exception to the open prose). Teardown is drop; each instance
/// shares nothing with any other.
pub fn new() -> Self {
Self {
state: Arc::new(State::new()),
wakes: Arc::new(WakeBus::new()),
clock: Arc::new(Clock::new()),
}
}
#[cfg(test)]
pub(crate) fn wakes(&self) -> &Arc<WakeBus> {
&self.wakes
}
#[cfg(test)]
pub(crate) fn clock(&self) -> &Arc<Clock> {
&self.clock
}
}
impl Drop for MemStore {
fn drop(&mut self) {
self.wakes.close();
}
}
impl Store for MemStore {
fn begin_tx(&self) -> BoxedFuture<'_, Result<Box<dyn TxHandle + Send>>> {
let handle = MemTxHandle::begin(self.state.clone(), self.clock.clone(), self.wakes.clone());
Box::pin(async move { Ok(Box::new(handle) as Box<dyn TxHandle + Send>) })
}
fn notify<'a>(&'a self, channel: &str, payload: Value) -> BoxedFuture<'a, Result<()>> {
crate::notify::notify(self.wakes.clone(), channel, payload)
}
fn listen<'a>(
&'a self,
channel: &str,
) -> BoxedFuture<'a, Result<Box<dyn alkstore::WakeReceiver>>> {
let wakes = self.wakes.clone();
let owned = channel.to_string();
Box::pin(async move { crate::notify::listen(wakes, &owned) })
}
fn stream<'a>(
&'a self,
name: &str,
) -> BoxedFuture<'a, Result<Box<dyn alkstore::StreamHandle>>> {
let handle = alkstore::validate_shared_name(name).map(|()| {
MemStreamHandle::new(
name.to_string(),
self.state.clone(),
self.clock.clone(),
self.wakes.clone(),
)
});
Box::pin(async move { handle.map(|h| Box::new(h) as Box<dyn alkstore::StreamHandle>) })
}
fn queue<'a>(
&'a self,
name: &str,
opts: QueueOpts,
) -> BoxedFuture<'a, Result<Box<dyn alkstore::Queue>>> {
let handle = alkstore::validate_shared_name(name).map(|()| {
Box::new(MemQueueHandle::new(
name.to_string(),
opts,
self.state.clone(),
self.clock.clone(),
)) as Box<dyn alkstore::Queue>
});
Box::pin(async move { handle })
}
fn outbox<'a>(&'a self, name: &str) -> BoxedFuture<'a, Result<Box<dyn alkstore::Outbox>>> {
let handle = alkstore::validate_shared_name(name).map(|()| {
Box::new(crate::outbox::MemOutbox::new(
name.to_string(),
self.state.clone(),
self.clock.clone(),
)) as Box<dyn alkstore::Outbox>
});
Box::pin(async move { handle })
}
fn try_lock<'a>(
&'a self,
name: &str,
owner: &str,
ttl: i64,
) -> BoxedFuture<'a, Result<Option<Box<dyn alkstore::Lock>>>> {
let outcome =
crate::lock::try_lock(self.state.clone(), self.clock.clone(), name, owner, ttl);
Box::pin(async move { outcome })
}
fn schedule<'a>(
&'a self,
name: &str,
spec: &str,
queue: &str,
payload: Value,
opts: ScheduleOpts,
) -> BoxedFuture<'a, Result<alkstore::Schedule>> {
scheduler::schedule(
self.state.clone(),
self.clock.clone(),
name,
spec,
queue,
payload,
opts,
)
}
fn unschedule<'a>(&'a self, name: &str) -> BoxedFuture<'a, Result<bool>> {
scheduler::unschedule(self.state.clone(), name)
}
fn run_schedules<'a>(&'a self, stop: StopToken) -> BoxedFuture<'a, Result<()>> {
scheduler::run_schedules(self.state.clone(), self.clock.clone(), stop)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alkstore::EnqueueOpts;
#[test]
fn two_new_instances_share_nothing() {
let a = MemStore::new();
let b = MemStore::new();
assert!(
!std::sync::Arc::ptr_eq(&a.state, &b.state),
"the state cores are distinct instances"
);
assert!(
!std::sync::Arc::ptr_eq(&a.wakes, &b.wakes),
"the wake substrates are distinct instances"
);
assert!(
!std::sync::Arc::ptr_eq(&a.clock, &b.clock),
"the clocks are distinct instances"
);
}
#[test]
fn instances_observed_through_their_futures_share_nothing() {
let a = MemStore::new();
let b = MemStore::new();
a.clock().freeze(1_000_000);
assert_eq!(a.clock().unix_now(), 1_000_000, "instance a's clock froze");
assert!(
b.clock().unix_now() > 1_700_000_000,
"instance b's clock kept the process time"
);
}
#[test]
fn wake_substrates_do_not_cross_instances() {
let a = MemStore::new();
let b = MemStore::new();
a.wakes().fire_commit_wakes(vec!["orders".into()]);
let mut late_feed = b.wakes().attach("orders");
assert!(
late_feed.rx().try_recv().is_err(),
"instance a's commit fired before the feed attached — instance b's substrate never replayed it, and instance a's substrate is unreachable from b"
);
let mut a_feed = a.wakes().attach("orders");
a.wakes().fire_commit_wakes(vec!["orders".into()]);
assert!(
matches!(
a_feed.rx().try_recv(),
Ok(wake) if wake.channel == "orders"
),
"instance a's substrate routes its own attaches"
);
}
#[tokio::test(flavor = "current_thread")]
async fn engine_drop_closes_open_feeds() {
let store = MemStore::new();
let mut feed = store.wakes().attach("orders");
std::mem::drop(store);
assert!(
feed.rx().recv().await.is_none(),
"teardown is drop: the engine's close path disconnects every feed"
);
}
#[tokio::test(flavor = "current_thread")]
async fn constructor_entry_validation() {
let store = MemStore::new();
for bad in ["", " ", "__alkstore_q"] {
match store.queue(bad, QueueOpts::default()).await {
Err(alkstore::Error::InvalidName { .. } | alkstore::Error::ReservedName { .. }) => {
}
_other => panic!("queue({bad:?}) rejects"),
}
assert!(store.stream(bad).await.is_err());
assert!(store.outbox(bad).await.is_err());
}
// Nothing was created on a rejected name — the next queue with
// a fresh name starts clean (ids allocate nowhere).
let good = store.queue("good", QueueOpts::default()).await.unwrap();
let id = good
.enqueue(serde_json::json!(1), EnqueueOpts::default())
.await
.unwrap();
assert_eq!(id, 1, "ids count from 1");
}
#[tokio::test(flavor = "current_thread")]
async fn begin_tx_grants_a_boxed_handle_that_commits() {
let store = MemStore::new();
let mut tx = store.begin_tx().await.unwrap();
tx.enqueue_tx("q", EnqueueOpts::default(), serde_json::json!(1))
.await
.unwrap();
tx.commit().await.unwrap();
let q = store.queue("q", QueueOpts::default()).await.unwrap();
assert!(q.get_job(1).await.unwrap().is_some());
}
}