feat(store): the Store facade — constructor modes, size-threshold dispatch, the factory binding

The wave-2 facade skeleton and routing (tasks/store-construction.md):
the public surface consumers program against, with the ops methods
still to land against it in store-read-paths/gc-protocol/store-put-paths.

- src/store.rs — the public `Store` type (engine-blind, ADR-007/008/
  010) over crate-internal tier enums (`KvEngine`{mem}, `LargeEngine`{
  local}): the engine-representation design point decided as concrete
  + enum dispatch, generic rejected (pub(crate) engines, no dyn);
  the three constructor modes as named constructors — `mem_only`,
  `kv_only_mem`, `dual_tier_mem_local` (the ADR-012 §6.3
  dispatch-coverage shape) — with `StoreMode` introspection; the
  write-side routing seam (`tier_for`), the read-side fall-through
  order (`kv()` then `large()`, ADR-012 §6.1), and the `GcHome`
  GC-state field; the ADR-012 §3 fleet declaration ruled deferred to
  the engine waves (nothing fleet-valid is constructible yet)
- src/dispatch.rs — the pure routing function (`route`, at-or-below
  → kv), `DEFAULT_DISPATCH_THRESHOLD` (128 KiB) exported at the
  crate root, boundary-value unit tests (exactly-at-threshold,
  degenerate tunings, purity)
- src/gc.rs — `GcHome`: the store-instance GC-state home the
  gc-protocol task fills (in-process; fleet state stays on the
  engine-state seam per ADR-010 §2/ADR-012 §4)
- tests/conformance — the wave-2 binding made as anticipated: the
  factory's `Store` associated type collapsed into the concrete
  `alkblobs::Store` (`open` vends it directly), recorded in
  factory.rs + README; the wave-1 mock probe retired, replaced by
  mem-gated binding demo factories (binding.rs) over both
  constructible shapes

Verification: `cargo test` (default), `cargo test --features mem`,
`cargo test --all-features` green (41/53/53 unit + conformance
targets green in all three shapes); `cargo clippy --all-targets
-- -D warnings` green in all three feature shapes; `cargo fmt
--check` green; `cargo doc --no-deps` adds no warnings beyond the
pre-existing private-intra-doc-links class.
This commit is contained in:
glm-5.3-flash committed 2026-10-11 22:18:32 +00:00
1 parent 856d436f81
commit 532adfa707
9 files changed
+840 -84

No files matched your search

+16 -9
View File
@@ -95,11 +95,16 @@ row with its own owner stamp, still one per property.
the temp dir, dropping the schema, closing the pool — and owns the
isolated-ness guarantee above.
- Factories are `Send + Sync`: shareable across row runs.
- The wave-2 binding note: the trait's `Store` associated type is the
stub's placeholder for the `Store` facade type (wave 2). Wave 2's
first column binds it; if binding reveals a shape correction (e.g.
the associated type collapses to the concrete facade type), that is
wave 2's to make and record in this README and `factory.rs`.
- **The wave-2 binding — made and recorded.** The `Store` facade type
landed with wave 2's store-construction task as a concrete type
(`alkblobs::Store`, engine-blind; src/store.rs holds the engine-
representation design point). The binding made the anticipated
shape correction: the skeleton's `Store` associated type collapsed
into the concrete facade — `open()` vends `alkblobs::Store`
directly (factory.rs records the ruling; the wave-1 mock probe
retired with it, replaced by the mem-gated binding demo factories
in `main.rs`). A new engine column binds by implementing the trait;
there is no longer a vendible type to choose.
## The SKIP posture
@@ -146,7 +151,9 @@ and first run in wave 6.
## Current state
The harness skeleton is compile-able with **zero rows** (this task's
scoped deliverable): the trait shape, the stamp convention, and the
backlog registry are in place; the rows arrive with wave 2's mem
column.
The harness skeleton is compile-able with **zero rows**: the trait
shape, the stamp convention, and the backlog registry are in place;
the wave-2 factory binding is **made** — `open()` vends the concrete
`alkblobs::Store` facade (see §The factory contract), demonstrated by
the mem-gated binding demo factories. The rows arrive with wave 2's
mem column.
+140
View File
@@ -0,0 +1,140 @@
//! The wave-2 factory binding demonstrator (mem-gated — mem is the
//! skip-free reference shape): the concrete `Store` facade vended by
//! [`StoreFactory::open`] in both constructible wave-2 shapes, the
//! factory contract's isolation + idempotent-teardown requirements
//! exercised, the wave-1 mock probe retired by the binding. The real
//! columns carry these factories forward with rows (suite-column-mem,
//! suite-column-local).
#![allow(dead_code)]
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::factory::StoreFactory;
use alkblobs::{Error, Store, StoreMode, DEFAULT_DISPATCH_THRESHOLD};
/// The binding demo factory, mem-only mode: a fresh `BTreeMap`-backed
/// store per `open` (isolated by construction — the ephemeral engine
/// shares nothing across instances), teardown a no-op. Mem is the
/// skip-free reference shape, so this column never skips.
struct MemOnlyBinding;
impl StoreFactory for MemOnlyBinding {
fn name(&self) -> &'static str {
"mem-only (binding demo)"
}
async fn open(&self) -> Result<Store, Error> {
Ok(Store::mem_only())
}
async fn teardown(&self) -> Result<(), Error> {
Ok(())
}
}
/// A fresh temp root per factory `open` — the isolated backing for the
/// dual-tier binding demo. No Drop guard: teardown owns removal (the
/// factory contract's ownership; a factory dropped without teardown
/// leaks its temp roots — harmless temp debris, and teardown makes
/// sense over the abandoned instance per the contract).
fn fresh_temp_root() -> PathBuf {
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!(
"alkblobs-conformance-{}-{:x}-{seq}",
std::process::id(),
nanos
));
std::fs::create_dir_all(&dir).expect("temp root created");
dir
}
/// The binding demo factory, dual-tier mode (the ADR-012 §6.3
/// dispatch-coverage shape, `kv = mem` + `large = local` at the default
/// threshold): a fresh temp directory per `open` so two opens never
/// share state, teardown removing every root it has vended (idempotent
/// — a repeated teardown is a no-op) and safe against abandoned
/// instances (each root is private to its open, so a half-open
/// instance's root is removable the same way).
struct DualTierBinding {
roots: Mutex<Vec<PathBuf>>,
}
impl Default for DualTierBinding {
fn default() -> Self {
DualTierBinding {
roots: Mutex::new(Vec::new()),
}
}
}
impl StoreFactory for DualTierBinding {
fn name(&self) -> &'static str {
"dual-tier mem+local (binding demo)"
}
async fn open(&self) -> Result<Store, Error> {
let path = fresh_temp_root();
let store = Store::dual_tier_mem_local(path.clone(), DEFAULT_DISPATCH_THRESHOLD).await?;
self.roots
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(path);
Ok(store)
}
async fn teardown(&self) -> Result<(), Error> {
for root in self
.roots
.lock()
.unwrap_or_else(|e| e.into_inner())
.drain(..)
{
match std::fs::remove_dir_all(&root) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(Error::Io(e.to_string())),
}
}
Ok(())
}
}
/// The binding holds: the demo factory vends the concrete facade in
/// the mem-only mode, reports its identity, and tears down idempotently.
#[tokio::test]
async fn binding_factory_vends_the_concrete_store_facade() {
let factory = MemOnlyBinding;
assert_eq!(factory.name(), "mem-only (binding demo)");
let store = factory.open().await.expect("open vends the store");
assert_eq!(store.mode(), StoreMode::MemOnly);
factory.teardown().await.expect("teardown is idempotent");
factory.teardown().await.expect("teardown repeats cleanly");
}
/// The binding holds in dual-tier mode too: every `open` is isolated
/// (fresh backing), the vend store reports the dispatch-coverage
/// shape, and teardown across open-then-teardown cycles is idempotent.
#[tokio::test]
async fn dual_tier_binding_factory_vends_isolated_stores() {
let factory = DualTierBinding::default();
assert_eq!(factory.name(), "dual-tier mem+local (binding demo)");
for _ in 0..2 {
let store = factory.open().await.expect("open vends the store");
assert_eq!(store.mode(), StoreMode::DualTier, "dispatch-coverage shape");
}
factory
.teardown()
.await
.expect("teardown removes the roots");
factory.teardown().await.expect("teardown repeats cleanly");
}
+28 -25
View File
@@ -8,53 +8,56 @@
//! the suite README §The factory contract. Engines implement this in
//! wave 2+'s column wiring; rows take factories, never stores.
//!
//! **The wave-2 binding.** The `Store` facade type arrives in wave 2
//! (store-api.md §What this is). This trait's `Store` associated type
//! is the skeleton's binding point for it: wave 2's first column (mem)
//! binds it to the real facade, and if binding reveals a shape
//! correction — most plausibly the associated type collapsing to the
//! concrete facade type across all factories — that is wave 2's to
//! make and record here and in the README.
//! **The wave-2 binding — made, recorded.** The `Store` facade type
//! landed with wave 2's store-construction task as a concrete type
//! (`alkblobs::Store`) whose engine representation is crate-internal
//! enum dispatch over the closed five-engine set — the collapse this
//! skeleton's note anticipated ("most plausibly the associated type
//! collapsing to the concrete facade type across all factories"). The
//! binding made that correction: there **is no `Store` associated
//! type** — [`StoreFactory::open`] vends the concrete facade type
//! directly, so every impl binds by construction and no factory can
//! vend anything else. The original associated-type shape was
//! plausible when the facade was unwritten and possibly generic; with
//! the concrete engine-blind type, a second vendible type would be the
//! surprise, so the placeholder collapsed into the contract.
//!
//! The SKIP posture is a factory-owner responsibility (README §SKIP
//! posture): SQL-attached factories probe their server before rows
//! run; a unreachable server skips the column cleanly, while the
//! run; an unreachable server skips the column cleanly, while the
//! engine wave's acceptance requires green against the server.
#![allow(dead_code)]
use std::future::Future;
use alkblobs::Error;
use alkblobs::{Error, Store};
/// Vends fresh, isolated store instances to the suite's rows.
///
/// Implementers — the engine columns' wiring — yield an instance of
/// the `Store` facade over an isolated backing store (fresh temp
/// directory, pool, file, or schema per call, so a property's rows
/// never outlive their run into another's) plus a teardown disposing
/// of the backing store. Implementations must be `Send + Sync` so one
/// factory instance can be shared across row runs.
/// the concrete `Store` facade over an isolated backing store (fresh
/// temp directory, pool, file, or schema per call, so a property's
/// rows never outlive their run into another's) plus a teardown
/// disposing of the backing store. Implementations must be `Send +
/// Sync` so one factory instance can be shared across row runs.
///
/// Rows call [`StoreFactory::open`], drive their assertions, and call
/// [`StoreFactory::teardown`] before returning; a failing assertion
/// panics the row (no teardown on that path), which is why teardown is
/// required to be idempotent and safe against an abandoned instance.
pub trait StoreFactory: Send + Sync {
/// The store instance type this factory vends — the wave-2 `Store`
/// facade's binding point (see the module docs).
type Store: Send;
/// The column's identity for reporting — which configuration rows
/// ran against (the constructor-mode vocabulary of ADR-011 §4,
/// once wave 2 names it). Rows never branch on it; the suite's
/// reporting and the SKIP posture's per-column bookkeeping do.
/// ran against (the constructor-mode vocabulary of ADR-011 §4).
/// Rows never branch on it; the suite's reporting and the SKIP
/// posture's per-column bookkeeping do.
fn name(&self) -> &'static str;
/// Vend a fresh, isolated store instance. A failure here is an
/// environment failure (e.g. the SKIP posture's unreachable
/// server), not a row failure.
fn open(&self) -> impl Future<Output = Result<Self::Store, Error>> + Send;
/// Vend a fresh, isolated store instance — the concrete `alkblobs::Store`
/// facade (the wave-2 binding, made and recorded above). A failure
/// here is an environment failure (e.g. the SKIP posture's
/// unreachable server), not a row failure.
fn open(&self) -> impl Future<Output = Result<Store, Error>> + Send;
/// Tear down the backing store behind this factory instance.
///
+14 -47
View File
@@ -3,12 +3,18 @@
//! The suite is in-crate by decided point (docs/plans/implementation.md):
//! engines are features of one crate, so the harness is factory-
//! parameterized over engine constructors rather than a sibling suite
//! crate. This target currently carries the **skeleton only** — the
//! factory trait shape, the stamp convention, and the invariant backlog
//! registry (see [`README`](./README.md) for the conventions' normative
//! statement). Rows arrive with wave 2's first column (mem, the
//! contract-reference engine); if binding the `Store` facade type
//! reveals a shape correction, that is wave 2's to make and record.
//! crate. This target carries the skeleton (factory trait shape, the
//! stamp convention, the invariant backlog registry — see
//! [`README`](./README.md)) plus the **wave-2 factory binding**: the
//! `Store` facade type landed as the concrete `alkblobs::Store`
//! (engine-blind; src/store.rs holds the engine-representation design
//! point), and the skeleton's `Store` associated type collapsed into
//! it — `open` vends the concrete facade type directly, recorded in
//! `factory.rs` and the README. The wave-1 mock probe retired with it;
//! the binding demonstrator ([`binding`], mem-gated — mem is the
//! skip-free reference shape) proves the contract implementable
//! against the real type. Rows arrive with wave 2's columns (mem
//! first, the contract-reference engine).
//!
//! Items beyond the tests below are the wave-2 drivers' surface, hence
//! the scoped `#![allow(dead_code)]` — it shrinks as rows land.
@@ -16,50 +22,11 @@
#![allow(dead_code)]
mod backlog;
#[cfg(feature = "mem")]
mod binding;
mod factory;
mod stamp;
use factory::StoreFactory;
/// The harness probe factory: not an engine — a minimal mock proving
/// the [`StoreFactory`] contract is implementable with async methods
/// and reports the identity `name()` carries (the alkstore harness's
/// "exemplar against a trivial in-crate mock" posture, sized down to
/// the skeleton's no-rows state).
struct Probe {
label: &'static str,
}
impl StoreFactory for Probe {
type Store = ();
fn name(&self) -> &'static str {
self.label
}
async fn open(&self) -> Result<Self::Store, alkblobs::Error> {
Ok(())
}
async fn teardown(&self) -> Result<(), alkblobs::Error> {
Ok(())
}
}
#[test]
fn probe_factory_implements_the_contract() {
let factory = Probe { label: "probe" };
assert_eq!(factory.name(), "probe", "factory identity is reportable");
}
#[tokio::test]
async fn probe_factory_open_teardown_lifecycle() {
let factory = Probe { label: "probe" };
factory.open().await.expect("open vends the instance");
factory.teardown().await.expect("teardown is idempotent");
factory.teardown().await.expect("teardown repeats cleanly");
}
#[test]
fn stamp_marker_matches_convention() {
assert_eq!(stamp::STAMP_MARKER, "Contract stamp:");