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
+118
@@ -1,2 +1,120 @@
|
||||
//! Size-threshold dispatch between the kv and large tiers (ADR-003):
|
||||
//! routing as a pure function of content length.
|
||||
//!
|
||||
//! **The routing seam the facade programs through.** [`route`] is the
|
||||
//! pure function — `route(len, threshold) → Tier`, **at-or-below the
|
||||
//! threshold stays on the kv tier** (the boundary's pinned reading:
|
||||
//! store-put-paths' "≤ threshold → kv; > threshold → large") — and
|
||||
//! [`DEFAULT_DISPATCH_THRESHOLD`] is the shipped default, 128 KiB, the
|
||||
//! conservative edge of the measured ~128–256 KiB crossover zone (POC #3
|
||||
//! A4). Purity is the ADR-003 invariant: same content ⇒ same length ⇒
|
||||
//! same tier, re-puts are deterministic, no content ever migrates.
|
||||
//!
|
||||
//! The threshold is **constructor-tunable** (a per-deployment media
|
||||
//! re-tuning, never an API change): the facade's mode constructors take
|
||||
//! it explicitly and default their docs to this constant. Read-side
|
||||
//! consult order — kv first, then large — is the facade's fall-through
|
||||
//! general rule (ADR-012 §6.1), carried on the `Store` facade type
|
||||
//! ([`crate::store`]); mode-specific behavior (kv-only's put-time
|
||||
//! rejection, mem-only's no-threshold posture) lives on the facade's
|
||||
//! mode machinery, not here: this module routes lengths, it does not
|
||||
//! own modes.
|
||||
//!
|
||||
//! The `#![allow(dead_code)]` below marks the seam's scope: `route` is
|
||||
//! driven by the facade's write path (store-put-paths) and consulted by
|
||||
//! the facade's read paths; the allow shrinks as those land.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
/// The default dispatch threshold in bytes: 128 KiB — the conservative
|
||||
/// edge of the measured ~128–256 KiB crossover zone (ADR-003; POC #3
|
||||
/// finding A4's first-party benchmark). Constructor-tunable per
|
||||
/// deployment media; retuning is a constructor parameter, never an API
|
||||
/// change.
|
||||
pub const DEFAULT_DISPATCH_THRESHOLD: u64 = 128 * 1024;
|
||||
|
||||
/// The two tiers a content length routes between. `pub(crate)`: the
|
||||
/// enum is the routing *answer*, not the engine — tier names the
|
||||
/// routing class (ADR-011 §1), and which engine backs the tier is the
|
||||
/// constructor's choice, never this type's business.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Tier {
|
||||
/// The kv tier — at-or-below-threshold content.
|
||||
Kv,
|
||||
/// The large tier — over-threshold content.
|
||||
Large,
|
||||
}
|
||||
|
||||
/// The pure routing function: over the threshold goes to the large
|
||||
/// tier; **at or below** (exactly-at-threshold included) stays on the
|
||||
/// kv tier. The boundary reading is pinned here and boundary-tested
|
||||
/// below — `store-put-paths` and the read paths consume this exact
|
||||
/// reading through the facade's seam.
|
||||
///
|
||||
/// Total purity discipline (ADR-003): a pure function of `len` — no
|
||||
/// engine input, no namespace, no state. Same length is the same tier
|
||||
/// forever, across mixed put paths, re-puts, and restarts.
|
||||
pub(crate) fn route(len: u64, threshold: u64) -> Tier {
|
||||
if len > threshold {
|
||||
Tier::Large
|
||||
} else {
|
||||
Tier::Kv
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The shipped default is 128 KiB — pinned numerically (131072).
|
||||
#[test]
|
||||
fn default_threshold_is_128_kib() {
|
||||
assert_eq!(DEFAULT_DISPATCH_THRESHOLD, 131_072);
|
||||
assert_eq!(DEFAULT_DISPATCH_THRESHOLD, 128 * 1024);
|
||||
}
|
||||
|
||||
/// The pinned boundary reading: exactly-at-threshold routes to the
|
||||
/// kv tier (≤ threshold → kv); one over routes large.
|
||||
#[test]
|
||||
fn exactly_at_threshold_routes_kv_one_over_routes_large() {
|
||||
assert_eq!(route(4096, 4096), Tier::Kv);
|
||||
assert_eq!(route(4095, 4096), Tier::Kv);
|
||||
assert_eq!(route(4097, 4096), Tier::Large);
|
||||
}
|
||||
|
||||
/// Boundary values across the domain: zero length, the default
|
||||
/// threshold, and the degenerate tunings a constructor may pass —
|
||||
/// all route deterministically.
|
||||
#[test]
|
||||
fn boundary_values_route_deterministically() {
|
||||
assert_eq!(route(0, DEFAULT_DISPATCH_THRESHOLD), Tier::Kv);
|
||||
assert_eq!(route(131_071, DEFAULT_DISPATCH_THRESHOLD), Tier::Kv);
|
||||
assert_eq!(route(131_072, DEFAULT_DISPATCH_THRESHOLD), Tier::Kv);
|
||||
assert_eq!(route(131_073, DEFAULT_DISPATCH_THRESHOLD), Tier::Large);
|
||||
|
||||
assert_eq!(route(0, 0), Tier::Kv, "zero-length content always kv");
|
||||
assert_eq!(
|
||||
route(1, 0),
|
||||
Tier::Large,
|
||||
"threshold 0: everything else large"
|
||||
);
|
||||
|
||||
assert_eq!(route(0, u64::MAX), Tier::Kv, "max threshold: nothing large");
|
||||
assert_eq!(
|
||||
route(u64::MAX, u64::MAX),
|
||||
Tier::Kv,
|
||||
"at-threshold is kv even at MAX"
|
||||
);
|
||||
}
|
||||
|
||||
/// Purity (the ADR-003 invariant): the same length is the same tier
|
||||
/// on every call — routing derives from inputs alone.
|
||||
#[test]
|
||||
fn routing_is_a_pure_function_of_length() {
|
||||
let len = 103_487;
|
||||
let first = route(len, DEFAULT_DISPATCH_THRESHOLD);
|
||||
for _ in 0..32 {
|
||||
assert_eq!(route(len, DEFAULT_DISPATCH_THRESHOLD), first);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,19 @@ use crate::backend::Backend;
|
||||
use crate::facade::PinState;
|
||||
use crate::Error;
|
||||
|
||||
/// The store-instance GC state home — held as a field of the public
|
||||
/// facade type ([`Store`](crate::store::Store)). ADR-011 §1's vocabulary
|
||||
/// and ADR-010 §2's ownership rule together: the store instance owns
|
||||
/// all GC state, so in-process GC state carries on the facade
|
||||
/// instance itself. The wave-2 gc-protocol task fills it — the pin
|
||||
/// registry (per-key refcounts under the arbitration lock), registered
|
||||
/// liveness sources, the protect callback, and the in-process delete
|
||||
/// window (ADR-005's protocol inline, ADR-012 §5's non-SQL staging) —
|
||||
/// and drives sweep/direct-delete through the facade. Fleet state never
|
||||
/// lands here: it rides the engine-state companion seam below
|
||||
/// (ADR-010 §2), hosted on the fleet's kv engine (ADR-012 §4).
|
||||
pub(crate) struct GcHome;
|
||||
|
||||
/// A delete-window candidate staged into the durable window: the key and
|
||||
/// the time it becomes visible for deletion — exactly the
|
||||
/// `gc_window (key, visible_after)` row pair (ADR-005's delete-window
|
||||
|
||||
@@ -6,11 +6,14 @@ pub mod gc;
|
||||
pub mod hashing;
|
||||
pub mod keys;
|
||||
pub mod local;
|
||||
pub mod store;
|
||||
|
||||
pub use dispatch::DEFAULT_DISPATCH_THRESHOLD;
|
||||
pub use error::Error;
|
||||
pub use facade::{Batch, EntryMeta, GcAbortCause, Pin, PinState, SweepReport};
|
||||
pub use hashing::{Algorithm, Digest};
|
||||
pub use keys::Key;
|
||||
pub use store::{Store, StoreMode};
|
||||
|
||||
#[cfg(feature = "mem")]
|
||||
pub mod mem;
|
||||
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
//! The `Store` facade type (store-api.md §What this is): the public
|
||||
//! surface consumers program against — constructor modes and the
|
||||
//! size-threshold dispatch seam. **Skeleton and routing only**: the ops
|
||||
//! methods land against this type in `store-read-paths` (get/stat/
|
||||
//! read_range/has/list with the fall-through general rule),
|
||||
//! `gc-protocol` (sweep, direct delete, the pin/liveness machinery),
|
||||
//! and `store-put-paths` (put, the kv-only rejection behavior). This
|
||||
//! module owns the engine representation and the constructor-mode
|
||||
//! machinery; the pure routing function lives in [`crate::dispatch`].
|
||||
//!
|
||||
//! **Named design point — engine representation: concrete `Store`,
|
||||
//! crate-internal enum dispatch.** A `Store<Kv: Backend, Large: Backend>`
|
||||
//! generic was the alternative: rejected. The engines are statically
|
||||
//! known and in-crate (no `dyn` anywhere — wave-1 design point 3), and
|
||||
//! they are `pub(crate)` behind the crate-internal [`Backend`][crate::backend::Backend]
|
||||
//! trait — a generic facade parameterized over `Backend` implementers
|
||||
//! would leak engine types through the public surface, force consumers
|
||||
//! to name engines they must never see (the facade is **engine-blind**,
|
||||
//! ADR-007/008/010), or push `dyn` onto the surface. The concrete type
|
||||
//! holds two crate-internal engine-holding enums ([`KvEngine`],
|
||||
//! [`LargeEngine`]) dispatching over the closed five-engine set; the
|
||||
//! conformance factory's binding note anticipated exactly this collapse
|
||||
//! (tests/conformance/factory.rs — "most plausibly the associated type
|
||||
//! collapsing to the concrete facade type across all factories"). The
|
||||
//! public surface is engine-blind: consumers name modes and engines
|
||||
//! only through named constructors, feature-gated with their engine
|
||||
//! (`mem_only` needs feature `mem`; the sqlite/postgres/pg-lo
|
||||
//! constructors arrive with waves 3–5 and slot into the same enums and
|
||||
//! constructor pattern without reshaping — adding an enum variant and
|
||||
//! a named constructor is slotting in).
|
||||
//!
|
||||
//! **Constructor modes** (ADR-011 §4; ADR-012 §6.3):
|
||||
//!
|
||||
//! - **dual-tier** (the default): kv + large, size-threshold dispatch
|
||||
//! via [`crate::dispatch::route`]. The wave-2 constructible shape is
|
||||
//! `kv = mem` + `large = local` — the dispatch-coverage test shape
|
||||
//! ADR-012 §6.3 names (both tiers load-bearing). Over-threshold
|
||||
//! content routes to the large tier; at-or-below to kv.
|
||||
//! - **kv-only**: no large tier; over-threshold puts reject **at put
|
||||
//! time** (ADR-012 §6.2 — known-length immediately; unknown-length at
|
||||
//! mid-stream threshold overflow). The rejection behavior itself
|
||||
//! lands with the put seam; the threshold this mode carries is the
|
||||
//! rejection point's value.
|
||||
//! - **mem-only**: the mem engine alone, **no dispatch threshold in
|
||||
//! effect** (everything is local and small) — a testing/embedder-
|
||||
//! ephemeral posture, never production. Structurally distinct from
|
||||
//! kv-only precisely by that threshold absence.
|
||||
//!
|
||||
//! Modes are validated structurally — unrepresentable states are
|
||||
//! unrepresentable by construction: dual-tier's signature constructs
|
||||
//! the large engine itself, mem-only's constructor exists only with
|
||||
//! feature `mem` and constructs the mem engine — and every instance
|
||||
//! carries its mode for introspection ([`Store::mode`]).
|
||||
//!
|
||||
//! **The fleet declaration defers to the engine waves.** ADR-012 §3
|
||||
//! rules fleet mode a constructor declaration (`fleet: true`), never
|
||||
//! inferred — but it is a constructor *error* for every non-fleet-valid
|
||||
//! configuration, and no fleet-valid configuration is constructible yet
|
||||
//! (mem is never fleet-valid; `local` needs the shared-media
|
||||
//! declaration of ADR-008 §3; the fleet-valid engines — postgres, pg-lo
|
||||
//! — are waves 4–5). Adding the declaration then is additive; carrying
|
||||
//! a flag that could only ever be rejected today would be dead
|
||||
//! machinery.
|
||||
//!
|
||||
//! **The dispatch seam the ops tasks program through:** tier selection
|
||||
//! for a given length ([`Store::tier_for`] composing
|
||||
//! [`crate::dispatch::route`] per mode), the fall-through consult
|
||||
//! order — **kv first, then large** — that every key-addressed op
|
||||
//! consumes (ADR-012 §6.1's general rule; [`Store::kv`] then
|
||||
//! [`Store::large`]), and the GC state home ([`crate::gc::GcHome`])
|
||||
//! the gc-protocol task fills.
|
||||
//!
|
||||
//! The `#![allow(dead_code)]` below marks the skeleton's scope: the
|
||||
//! accessors and engine enums exist for the ops tasks' drivers, and the
|
||||
//! kv engine set is empty in default builds (sqlite is wave 3). The
|
||||
//! allow shrinks as the ops tasks land.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::dispatch::{route, Tier};
|
||||
use crate::gc::GcHome;
|
||||
use crate::local::LocalBackend;
|
||||
|
||||
#[cfg(feature = "mem")]
|
||||
use crate::mem::MemBackend;
|
||||
|
||||
#[cfg(feature = "mem")]
|
||||
use crate::Error;
|
||||
|
||||
#[cfg(feature = "mem")]
|
||||
use std::path::PathBuf;
|
||||
|
||||
use self::StoreMode::{DualTier, KvOnly, MemOnly};
|
||||
|
||||
/// The store instance — the public facade above the tier engines
|
||||
/// (ADR-011 §1's vocabulary: *one running instantiation of the store
|
||||
/// facade — one engine set per the constructor mode, plus dispatch and
|
||||
/// GC state*; **the GC arbitration domain** for single-instance
|
||||
/// topologies).
|
||||
///
|
||||
/// Engine-blind by construction: the engines are held in crate-internal
|
||||
/// enums dispatched statically (see the module's engine-representation
|
||||
/// design point); consumers name modes only. The GC state home
|
||||
/// ([`crate::gc::GcHome`]) rides as a field — the store instance owns
|
||||
/// all GC state (ADR-010 §2), which the gc-protocol task fills.
|
||||
pub struct Store {
|
||||
/// The kv-tier engine — the first consult in every fall-through.
|
||||
kv: KvEngine,
|
||||
/// The large-tier engine — `None` in single-tier modes.
|
||||
large: Option<LargeEngine>,
|
||||
/// The dispatch threshold in effect where the mode has one; mem-only
|
||||
/// carries the shipped default but never consults it — "no dispatch
|
||||
/// threshold in effect" is the mode's ruling (ADR-011 §4), not a
|
||||
/// value absence.
|
||||
threshold: u64,
|
||||
/// The constructor mode; the dispatch/put/read/gc paths branch on it.
|
||||
mode: StoreMode,
|
||||
/// The GC state home (ADR-010 §2: the store core owns GC state)
|
||||
/// — the gc-protocol task's machinery lands here.
|
||||
gc: GcHome,
|
||||
}
|
||||
|
||||
/// The constructor modes (ADR-011 §4's pinned set; ADR-012 §6.3's
|
||||
/// dual-tier ruling). Three variants, ADR-pinned — a new mode is a new
|
||||
/// ADR.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StoreMode {
|
||||
/// kv + large, size-threshold dispatch between them (the default).
|
||||
DualTier,
|
||||
/// kv alone; over-threshold puts reject at put time (ADR-012 §6.2).
|
||||
KvOnly,
|
||||
/// The mem engine alone, no dispatch threshold in effect — never
|
||||
/// production (ADR-011 §4).
|
||||
MemOnly,
|
||||
}
|
||||
|
||||
/// The kv-tier engine set — the closed per-tier engine enum of the
|
||||
/// engine-representation design point. Feature-gated variants slot in
|
||||
/// as their engine waves land: sqlite (wave 3), postgres (wave 4).
|
||||
pub(crate) enum KvEngine {
|
||||
/// The contract-reference ephemeral engine (ADR-011 §2).
|
||||
#[cfg(feature = "mem")]
|
||||
Mem(MemBackend),
|
||||
}
|
||||
|
||||
/// The large-tier engine set. `local` is ungated (dependency-free,
|
||||
/// always present, ADR-013 §4); pg-lo slots in at wave 5.
|
||||
pub(crate) enum LargeEngine {
|
||||
/// The sharded-filesystem engine (ADR-003/008).
|
||||
Local(LocalBackend),
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// **mem-only mode** (ADR-011 §4): the mem engine alone, no
|
||||
/// dispatch threshold in effect — a testing/embedder-ephemeral
|
||||
/// posture, never production. Every length routes to the (sole)
|
||||
/// mem engine; there is no large tier and nothing to reject.
|
||||
#[cfg(feature = "mem")]
|
||||
pub fn mem_only() -> Store {
|
||||
Store {
|
||||
kv: KvEngine::Mem(MemBackend::default()),
|
||||
large: None,
|
||||
threshold: crate::dispatch::DEFAULT_DISPATCH_THRESHOLD,
|
||||
mode: MemOnly,
|
||||
gc: GcHome,
|
||||
}
|
||||
}
|
||||
|
||||
/// **kv-only mode** (ADR-011 §4; ADR-012 §6.2) with the mem kv
|
||||
/// engine: no large tier; `threshold` is the put-time rejection
|
||||
/// point — known-length puts over it reject immediately,
|
||||
/// unknown-length puts at mid-stream threshold overflow (the put
|
||||
/// seam's behavior; zero residue on either rejection). Routing in
|
||||
/// this mode is kv for every length.
|
||||
#[cfg(feature = "mem")]
|
||||
pub fn kv_only_mem(threshold: u64) -> Store {
|
||||
Store {
|
||||
kv: KvEngine::Mem(MemBackend::default()),
|
||||
large: None,
|
||||
threshold,
|
||||
mode: KvOnly,
|
||||
gc: GcHome,
|
||||
}
|
||||
}
|
||||
|
||||
/// **dual-tier mode** (the default, ADR-011 §4) in the wave-2
|
||||
/// constructible shape — `kv = mem` + `large = local`, the
|
||||
/// dispatch-coverage test shape ADR-012 §6.3 names: both tiers
|
||||
/// load-bearing, routed by size-threshold dispatch at `threshold`
|
||||
/// (constructor-tunable; [`crate::dispatch::DEFAULT_DISPATCH_THRESHOLD`]
|
||||
/// is the shipped default).
|
||||
///
|
||||
/// Constructing the large engine creates the root directory —
|
||||
/// fallible I/O, hence a fallible constructor. The mem kv engine
|
||||
/// cannot fail.
|
||||
///
|
||||
/// Not fleet-anything: mem is never fleet-valid (ADR-011 §4).
|
||||
#[cfg(feature = "mem")]
|
||||
pub async fn dual_tier_mem_local(
|
||||
root: impl Into<PathBuf>,
|
||||
threshold: u64,
|
||||
) -> Result<Store, Error> {
|
||||
Ok(Store {
|
||||
kv: KvEngine::Mem(MemBackend::default()),
|
||||
large: Some(LargeEngine::Local(LocalBackend::new(root).await?)),
|
||||
threshold,
|
||||
mode: DualTier,
|
||||
gc: GcHome,
|
||||
})
|
||||
}
|
||||
|
||||
/// The constructor mode (ADR-011 §4's vocabulary) — introspection
|
||||
/// for embedders and tests; the dispatch/put/read paths branch on
|
||||
/// it crate-internally.
|
||||
pub fn mode(&self) -> StoreMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
/// The active dispatch threshold — `Some` in dual-tier (the routing
|
||||
/// boundary) and kv-only (the put-time rejection point); `None` in
|
||||
/// mem-only mode ("no dispatch threshold in effect", ADR-011 §4).
|
||||
pub(crate) fn threshold(&self) -> Option<u64> {
|
||||
match self.mode {
|
||||
DualTier | KvOnly => Some(self.threshold),
|
||||
MemOnly => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// **Write-side tier selection for a given content length** — the
|
||||
/// dispatch seam's routing half (the put seam programs through
|
||||
/// this; read paths do not consult it). Dual-tier:
|
||||
/// [`crate::dispatch::route`]'s pure function at the instance's
|
||||
/// threshold (exactly-at-threshold stays kv — the pinned boundary
|
||||
/// reading). kv-only: kv for every length — the over-threshold
|
||||
/// *rejection* is the put seam's behavior keyed off
|
||||
/// [`Store::threshold`] (ADR-012 §6.2), not a routing answer.
|
||||
/// Mem-only: kv — there is no other tier and no threshold in
|
||||
/// effect.
|
||||
pub(crate) fn tier_for(&self, len: u64) -> Tier {
|
||||
match self.mode {
|
||||
DualTier => route(len, self.threshold),
|
||||
KvOnly | MemOnly => Tier::Kv,
|
||||
}
|
||||
}
|
||||
|
||||
/// **The fall-through consult order — kv first** (ADR-012 §6.1's
|
||||
/// general rule): every key-addressed op consults the kv tier
|
||||
/// first; large comes after ([`Store::large`]). A miss on both
|
||||
/// tiers resolves per op's shape (`None` on get/stat,
|
||||
/// `Err(Missing)` on read_range/delete).
|
||||
pub(crate) fn kv(&self) -> &KvEngine {
|
||||
&self.kv
|
||||
}
|
||||
|
||||
/// **…then large** — the second consult; `None` in single-tier
|
||||
/// modes (mem-only, kv-only), where large-tier content cannot
|
||||
/// exist.
|
||||
pub(crate) fn large(&self) -> Option<&LargeEngine> {
|
||||
self.large.as_ref()
|
||||
}
|
||||
|
||||
/// The GC state home (ADR-010 §2: the store core owns all GC
|
||||
/// state) — the gc-protocol task's pin registry, liveness sources,
|
||||
/// protect callback, and in-process delete window land here and
|
||||
/// drive the sweep and direct delete from this facade.
|
||||
pub(crate) fn gc(&self) -> &GcHome {
|
||||
&self.gc
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "mem"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dispatch::DEFAULT_DISPATCH_THRESHOLD;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// A fresh temp root per test (mirrors `local.rs`'s helper); Drop
|
||||
/// removes the whole tree idempotently.
|
||||
struct TempRoot(PathBuf);
|
||||
|
||||
fn temp_root(tag: &'static str) -> TempRoot {
|
||||
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-store-{tag}-{}-{:x}-{seq}",
|
||||
std::process::id(),
|
||||
nanos
|
||||
));
|
||||
fs::create_dir_all(&dir).expect("temp root created");
|
||||
TempRoot(dir)
|
||||
}
|
||||
|
||||
impl TempRoot {
|
||||
fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempRoot {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// mem-only constructs and validates: the ephemeral mode's shape —
|
||||
/// no large tier, no threshold in effect, kv routing for every
|
||||
/// length (ADR-011 §4).
|
||||
#[cfg(feature = "mem")]
|
||||
#[test]
|
||||
fn mem_only_constructs_and_validates() {
|
||||
let store = Store::mem_only();
|
||||
assert_eq!(store.mode(), MemOnly);
|
||||
assert_eq!(store.threshold(), None, "no dispatch threshold in effect");
|
||||
assert!(store.large().is_none(), "mem-only has no large tier");
|
||||
|
||||
for len in [0u64, 1, DEFAULT_DISPATCH_THRESHOLD + 1, u64::MAX] {
|
||||
assert_eq!(
|
||||
store.tier_for(len),
|
||||
Tier::Kv,
|
||||
"mem-only routes every length to its sole tier"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// kv-only constructs and validates: no large tier; the threshold
|
||||
/// is the *rejection point's* carrier (ADR-012 §6.2), not a routing
|
||||
/// answer — every length routes kv there, and the put seam rejects
|
||||
/// known-length over-threshold puts immediately (its behavior).
|
||||
#[cfg(feature = "mem")]
|
||||
#[test]
|
||||
fn kv_only_mem_constructs_and_validates() {
|
||||
let store = Store::kv_only_mem(1024);
|
||||
assert_eq!(store.mode(), KvOnly);
|
||||
assert_eq!(store.threshold(), Some(1024));
|
||||
assert!(store.large().is_none(), "kv-only has no large tier");
|
||||
|
||||
for len in [0u64, 1023, 1024, 1025, u64::MAX] {
|
||||
assert_eq!(
|
||||
store.tier_for(len),
|
||||
Tier::Kv,
|
||||
"kv-only routes every length to its sole tier"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The ADR-012 §6.3 dispatch-coverage shape: `kv = mem` +
|
||||
/// `large = local` dual-tier construction works, both tiers
|
||||
/// load-bearing, dispatch threshold active — boundary values route
|
||||
/// deterministically (at/at-below threshold kv, above large), and
|
||||
/// tunability is proven against a non-default threshold.
|
||||
#[cfg(feature = "mem")]
|
||||
#[tokio::test]
|
||||
async fn dual_tier_mem_local_constructs_and_routes() {
|
||||
let root = temp_root("dual-tier");
|
||||
let store = Store::dual_tier_mem_local(root.path(), 1024)
|
||||
.await
|
||||
.expect("dual-tier mem+local constructs");
|
||||
assert_eq!(store.mode(), DualTier);
|
||||
assert_eq!(store.threshold(), Some(1024));
|
||||
assert!(store.large().is_some(), "both tiers present");
|
||||
|
||||
for (len, tier) in [
|
||||
(0u64, Tier::Kv),
|
||||
(1023, Tier::Kv),
|
||||
(1024, Tier::Kv),
|
||||
(1025, Tier::Large),
|
||||
(u64::MAX, Tier::Large),
|
||||
] {
|
||||
assert_eq!(
|
||||
store.tier_for(len),
|
||||
tier,
|
||||
"length {len} routes deterministically"
|
||||
);
|
||||
assert_eq!(
|
||||
store.tier_for(len),
|
||||
tier,
|
||||
"same length ⇒ same tier (the purity invariant, re-checked)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The shipped default threshold is the constructor-tunable
|
||||
/// baseline: dual-tier at `DEFAULT_DISPATCH_THRESHOLD` routes at
|
||||
/// the 128 KiB boundary exactly as the pure function does.
|
||||
#[cfg(feature = "mem")]
|
||||
#[tokio::test]
|
||||
async fn dual_tier_routes_at_the_default_threshold() {
|
||||
let root = temp_root("dual-tier-default");
|
||||
let store = Store::dual_tier_mem_local(root.path(), DEFAULT_DISPATCH_THRESHOLD)
|
||||
.await
|
||||
.expect("dual-tier constructs at the default threshold");
|
||||
|
||||
for (len, tier) in [
|
||||
(131_071, Tier::Kv),
|
||||
(131_072, Tier::Kv),
|
||||
(131_073, Tier::Large),
|
||||
] {
|
||||
assert_eq!(store.tier_for(len), tier, "length {len} at 128 KiB");
|
||||
}
|
||||
}
|
||||
|
||||
/// The GC state home rides as a field of the store instance
|
||||
/// (ADR-010 §2 — the store instance owns GC state) and is
|
||||
/// reachable crate-internally: the gc-protocol task's machinery
|
||||
/// lands against this handle.
|
||||
#[cfg(feature = "mem")]
|
||||
#[test]
|
||||
fn gc_state_home_rides_on_the_foundation() {
|
||||
let store = Store::mem_only();
|
||||
let _ = store.gc();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: store-construction
|
||||
name: Store facade type — constructor modes and size-threshold dispatch
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [engine-mem, engine-local]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
@@ -87,8 +87,94 @@ and `store-put-paths` against this type.
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
- **Engine representation decided**: concrete `Store` holding two
|
||||
crate-internal engine enums (`KvEngine`: mem [feature-gated];
|
||||
`LargeEngine`: local [ungated]), not the generic
|
||||
`Store<Kv, Large>` — the generic would leak `pub(crate)` engine
|
||||
types through the public surface or force `dyn` (wave-1 design
|
||||
point 3); the facade stays engine-blind (ADR-007/008/010) and the
|
||||
conformance factory's binding note anticipated exactly this
|
||||
collapse. Waves 3–5 slot in by adding an enum variant + a named
|
||||
(feature-gated) constructor — no reshaping. Recorded in
|
||||
`src/store.rs` module docs.
|
||||
- **Constructor modes** land as named constructors, engines named by
|
||||
the constructor (public surface can't take `pub(crate)` engine
|
||||
handles): `mem_only()` / `kv_only_mem(threshold)` /
|
||||
`dual_tier_mem_local(root, threshold)` (the ADR-012 §6.3
|
||||
dispatch-coverage shape; fallible because the local root's creation
|
||||
is I/O). Modes validate structurally (unrepresentable states
|
||||
unrepresentable — dual-tier constructs the large engine itself) and
|
||||
every instance carries its mode (`Store::mode() → StoreMode`,
|
||||
public). The **fleet declaration (`fleet: true`, ADR-012 §3)
|
||||
defers to the engine waves**: it is a constructor *error* for every
|
||||
non-fleet-valid configuration, and none is constructible yet — the
|
||||
ruling is recorded in store.rs docs rather than carried as a
|
||||
dead-machinery flag.
|
||||
- **Threshold shape**: `None`-vs-value is the mode's ruling (mem-only
|
||||
= "no dispatch threshold in effect", ADR-011 §4), so the field is
|
||||
always a value and mem-only's `threshold()` returns `None` by mode
|
||||
branch — no unrepresentable `Option` state to `expect` away
|
||||
(AGENTS convention 2). The pure function is `dispatch::route(len,
|
||||
threshold)` with the pinned boundary reading **at-or-below → kv**;
|
||||
`DEFAULT_DISPATCH_THRESHOLD` (128 KiB) is exported at the crate
|
||||
root and constructors take the threshold explicitly (tunability
|
||||
proven by tests at a non-default threshold).
|
||||
- **Dispatch seam handed to the ops tasks**: write-side
|
||||
`Store::tier_for(len)` (dual-tier pure routing; kv-only routes kv
|
||||
for every length — the over-threshold *rejection* is the put seam's
|
||||
behavior keyed off `threshold()`, ADR-012 §6.2; mem-only kv), and
|
||||
read-side fall-through order as `kv()`-then-`large()` accessors
|
||||
documented as ADR-012 §6.1's general rule. The engine enum's
|
||||
`Backend` dispatch methods and the cursor/staging enum wrappers are
|
||||
deliberately not built here — they are store-read-paths and
|
||||
store-put-paths named design points (the public stream shape, the
|
||||
declared-len mismatch case) and adding methods to the enums is
|
||||
slotting in.
|
||||
- **GC state home**: `pub(crate) struct GcHome` in `src/gc.rs`, held
|
||||
as a `Store` field with a crate-internal `gc()` accessor — the
|
||||
gc-protocol task fills it (pin registry, liveness sources, protect
|
||||
callback, in-process window); fleet state explicitly never lands
|
||||
there (ADR-010 §2 / ADR-012 §4 host rule).
|
||||
- **Factory binding made and recorded**: the skeleton's `Store`
|
||||
associated type is gone — `StoreFactory::open` vends the concrete
|
||||
`alkblobs::Store` directly (`tests/conformance/factory.rs` + README
|
||||
§The factory contract record the ruling); the wave-1 mock probe
|
||||
retired, replaced by mem-gated binding demo factories
|
||||
(`tests/conformance/binding.rs`) covering mem-only and the
|
||||
dispatch-coverage dual-tier shape with isolation + idempotent
|
||||
teardown.
|
||||
- **Default-build note**: the kv engine set is empty until wave 3
|
||||
(sqlite), so `Store` exists in default builds but is
|
||||
unconstructible — constructors and construction tests are
|
||||
`#[cfg(feature = "mem")]`; the `#![allow(dead_code)]` scope notes on
|
||||
`dispatch.rs`/`store.rs` follow the house pattern and shrink as the
|
||||
ops tasks land.
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Implemented the `Store` facade skeleton in `src/store.rs`: the public
|
||||
engine-blind facade type holding the two crate-internal tier enums
|
||||
(`KvEngine` {mem}, `LargeEngine` {local}) per the recorded
|
||||
engine-representation ruling (concrete + enum dispatch, generic
|
||||
rejected), the three constructor modes as named constructors —
|
||||
`mem_only()`, `kv_only_mem(threshold)`, `dual_tier_mem_local(root,
|
||||
threshold)` (the ADR-012 §6.3 shape) — with `StoreMode` introspection,
|
||||
the write-side routing seam (`tier_for`) composing the pure
|
||||
`dispatch::route` (default 128 KiB, constructor-tunable, at-threshold →
|
||||
kv), the read-side fall-through order (`kv()` then `large()`, ADR-012
|
||||
§6.1), and the `GcHome` GC-state field the gc-protocol task fills
|
||||
(added to `src/gc.rs`). `dispatch.rs` grew the pure routing function
|
||||
plus `DEFAULT_DISPATCH_THRESHOLD` with boundary-value unit tests
|
||||
(exactly-at-threshold, degenerate tunings, purity). The conformance
|
||||
factory binding collapsed the `Store` associated type into the concrete
|
||||
facade (recorded in `factory.rs` + the suite README); the wave-1 probe
|
||||
became mem-gated binding demo factories. Tests: mem-only/kv-only/
|
||||
dual-tier construction + validation, threshold tunability at a
|
||||
non-default value, dispatch boundary values, factory open/teardown
|
||||
lifecycle in both constructible shapes.
|
||||
|
||||
Verification: `cargo test` (default), `cargo test --features mem`,
|
||||
`cargo test --all-features` all green; `cargo clippy --all-targets
|
||||
-- -D warnings` green in all three feature shapes; `cargo fmt --check`
|
||||
green; `cargo doc --no-deps` clean of new warnings beyond the
|
||||
pre-existing private-intra-doc-links class.
|
||||
@@ -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