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

This commit is contained in:
glm-5.3-flash committed 2026-10-10 11:55:39 +00:00
1 parent 6632dcce1f
commit 1c3e96802f
14 files changed
+5375 -70

No files matched your search

+168 -25
View File
@@ -1,40 +1,48 @@
//! 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.
//! 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, honestly ephemeral ADR-016-shaped posture:
//! the dependency on `alkstore-mem` *is* the ephemerality
//! declaration).
//! 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` trait implementation is the seam task's artifact
//! (`mem-engine-seam-tx` — the tx overlay is the load-bearing half);
//! this type is the trait boundary's substrate.
//! 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 take it from
/// `MemStore::new`; once the seam task lands, they hold
/// `alkstore::Store` — the trait is the contract surface, never the
/// engine type.
/// 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 {
// Read by the wave-6 mechanism task code (the `Store` seam and the
// handles); the foundations build alone sees `state`/`clock` as dead.
#[allow(dead_code)]
pub(crate) state: Arc<State>,
pub(crate) wakes: Arc<WakeBus>,
#[allow(dead_code)]
pub(crate) clock: Arc<Clock>,
}
@@ -52,9 +60,9 @@ impl std::fmt::Debug for MemStore {
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.
/// 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()),
@@ -80,9 +88,110 @@ impl Drop for MemStore {
}
}
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() {
@@ -145,4 +254,38 @@ mod tests {
"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());
}
}