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
@@ -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");
|
||||
}
|
||||
Reference in new issue
Block a user