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:
1 parent
856d436f81
commit
532adfa707
9 files changed
+840
-84
No files matched your search
@@ -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.
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
@@ -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:");
|
||||
|
||||
Reference in new issue
Block a user