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