cubesys: durable WAL + ConcurrentStore (NDJSON, group-commit fsync, recovery log)

- Add persist.rs: std-only NDJSON snapshot of the HashBackend store
  (no serde) for the durable checkpoint + load_into_store replay.
- Add store.rs: ConcurrentStore = Mutex<HashBackend> live store + WAL
  (newline-delimited JSON, group-commit fsync, idempotent seq-numbered
  replay) + durable JSON checkpoint + bg flusher + startup replay.
- Recovery events (checkpoint failure, WAL fsync failure, WAL replay)
  are written to a recovery.ndjson you asked to keep as the written backup
  log, so any fall-back to JSON is recorded 'in writing'.
- Refactor cube-server to thread-per-connection over ConcurrentStore.
- query_doc_type / scan_prefix / linked_to / delete_raw added.

Verified: ./check (fmt, 7 unit tests, clippy -D warnings) all green;
./check stress drove 22,080 prog+run pairs (~368/s) over 60s, daemon
survived, latency prog~9us/run~13us mean.
This commit is contained in:
CUBELinux-2
2026-08-11 03:18:59 -04:00
parent bbaa36a32c
commit 7ccb29aa6a
5 changed files with 1107 additions and 204 deletions
+71 -59
View File
@@ -1,19 +1,22 @@
//! Shared CUBELinux-2 system command interpreter.
//!
//! This module holds the *single* implementation of the cube command language
//! (`prog`, `write`, `run`, `ls`, `stat`, `seal`, `open`). It is used by every
//! front-end — the local `cube` REPL, the `cubec` socket client, and the
//! `cube-server` daemon — so the behaviour can never drift between them.
//! (`prog`, `write`, `run`, `ls`, `stat`, `seal`, `open`, `query`). It is used
//! by every front-end — the local `cube` REPL, the `cubec` socket client, and
//! the `cube-server` daemon — so the behaviour can never drift between them.
//!
//! A [`Session`] wraps one [`CubeStore`] backend (currently the in-memory
//! [`HashBackend`]) and executes one command line at a time against it. The
//! daemon holds a single long-lived `Session`; the REPL holds a transient one.
//! A [`Session`] wraps one [`ConcurrentStore`] (a mutex-wrapped `CubeStore`
//! with a write-ahead log + scheduled checkpoint behind it). Because the store
//! is concurrent and durable, the daemon can serve many connections at once and
//! survives restarts. Each `exec` takes and returns an `Arc<ConcurrentStore>`
//! so the server can hand a cloned handle to each worker thread.
use crate::store::ConcurrentStore;
use cubecode::{CodeCell, Kind, Op, Vm};
use cubecoords::CubeHeader;
use cubecrypt::{CubeEnv, KeySlot, Selector, TransformId};
use cubestore::{CubeStore, HashBackend};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Instant;
/// Per-command latency accumulator (cumulative; the daemon reports these via
@@ -25,10 +28,12 @@ struct CmdStat {
max_ns: u128,
}
/// One cube command session: a store plus the command interpreter.
/// One cube command session: a concurrent, durable store plus the interpreter.
/// The REPL holds a transient one; the daemon shares a single `Arc<Session>`
/// across all connection threads.
pub struct Session {
store: CubeStore<HashBackend>,
/// Total commands executed since this session started (telemetry).
store: Arc<ConcurrentStore>,
/// Total commands executed under this session (telemetry).
calls: u64,
/// Per top-level command latency histogram (command name -> stats).
per_cmd: BTreeMap<String, CmdStat>,
@@ -41,36 +46,35 @@ impl Default for Session {
}
impl Session {
/// A fresh, empty session over an in-memory store.
/// A fresh, empty session over an in-memory (non-durable) store.
pub fn new() -> Self {
Session {
store: CubeStore::new(HashBackend::new()),
store: Arc::new(ConcurrentStore::memory()),
calls: 0,
per_cmd: BTreeMap::new(),
}
}
/// Access the underlying store (used by persistence to snapshot/restore).
pub fn store(&self) -> &CubeStore<HashBackend> {
&self.store
/// A session over a durable, concurrent store (used by the daemon).
pub fn with_store(store: Arc<ConcurrentStore>) -> Self {
Session {
store,
calls: 0,
per_cmd: BTreeMap::new(),
}
}
/// Mutable access to the underlying store (used by persistence to load).
pub fn store_mut(&mut self) -> &mut CubeStore<HashBackend> {
&mut self.store
/// Shared handle to the underlying concurrent store.
pub fn store(&self) -> Arc<ConcurrentStore> {
self.store.clone()
}
/// Snapshot of the session's telemetry: total commands serviced, per-command
/// latency distribution (mean/max in µs), and the occupancy of each `C`
/// namespace (number of records whose class axis equals `c`).
///
/// This is the "substantive" telemetry the daemon exposes — not just a
/// health ping. A monitoring pass can sample `stats` repeatedly and derive
/// request rates and latency histograms from the cumulative counters.
pub fn stats(&self) -> String {
let mut lines = Vec::new();
lines.push(format!("total commands serviced: {}", self.calls));
// per-command latency distribution
lines.push("per-command latency (µs, mean / max / count):".into());
if self.per_cmd.is_empty() {
lines.push(" (no commands timed yet)".into());
@@ -88,7 +92,6 @@ impl Session {
));
}
}
// per-C namespace occupancy (C axis 0 = Null control space)
let keys = self.store.keys();
let mut by_c: BTreeMap<u8, usize> = BTreeMap::new();
for k in &keys {
@@ -113,8 +116,6 @@ impl Session {
let t0 = Instant::now();
let cmd_name = line.split_whitespace().next().unwrap_or("").to_string();
let result = self.exec_inner(line);
// record telemetry regardless of ok/err (a failed command is still a
// serviced command and worth timing).
self.calls += 1;
let st = self.per_cmd.entry(cmd_name).or_default();
let elapsed = t0.elapsed().as_nanos();
@@ -130,12 +131,25 @@ impl Session {
fn exec_inner(&mut self, line: &str) -> Result<String, String> {
let mut it = line.split_whitespace();
let cmd = it.next().ok_or_else(|| "empty line".to_string())?;
let store = &self.store;
match cmd {
"stats" => {
// Substantive telemetry: command volume + latency distribution
// + per-C-namespace record occupancy. This is what makes the
// daemon measurable, not merely "healthy".
Ok(self.stats())
"stats" => Ok(self.stats()),
"query" => {
let dt = it
.next()
.ok_or_else(|| "query needs <doc_type>".to_string())?;
let coords = store.query_doc_type(dt);
if coords.is_empty() {
Ok(format!("query {dt} -> (no matches)"))
} else {
let names: Vec<String> =
coords.iter().map(|c| c.pack_u32().to_string()).collect();
Ok(format!(
"query {dt} -> {} matches: {}",
coords.len(),
names.join(" ")
))
}
}
"prog" => {
let path = it.next().ok_or_else(|| "prog needs <path>".to_string())?;
@@ -154,9 +168,9 @@ impl Session {
return Err("prog: no ops given".to_string());
}
let name = path.rsplit('/').next().unwrap_or(path);
let coord =
crate::store_code_cell(&mut self.store, path, Kind::Fn, name, &[], &ops)
.map_err(|e| e.to_string())?;
let coord = store
.put_code_cell(path, Kind::Fn, name, &[], &ops)
.map_err(|e| e.to_string())?;
Ok(format!(
"wrote program {path} -> coord {} ({} ops)",
coord.pack_u32(),
@@ -172,15 +186,17 @@ impl Session {
let code = cubecode::decode(&bytes)
.map_err(|e| format!("bytecode decode error: {e:?}"))?;
let name = path.rsplit('/').next().unwrap_or(path);
let coord =
crate::store_code_cell(&mut self.store, path, Kind::Fn, name, &[], &code)
.map_err(|e| e.to_string())?;
let coord = store
.put_code_cell(path, Kind::Fn, name, &[], &code)
.map_err(|e| e.to_string())?;
Ok(format!("wrote {path} -> coord {}", coord.pack_u32()))
}
"run" => {
let path = it.next().ok_or_else(|| "run needs <path>".to_string())?;
let cell = crate::load_code_cell(&self.store, path).map_err(|e| e.to_string())?;
let mut vm = Vm::new(self.store.clone());
let _coord = crate::path_to_czyx(path).map_err(|e| e.to_string())?;
let sn = store.read_snapshot();
let cell = crate::load_code_cell(&sn, path).map_err(|e| e.to_string())?;
let mut vm = Vm::new(sn);
let res = vm.run(cell.label);
let mut out = format!("run {path} => {res:?}");
if !vm.output().is_empty() {
@@ -193,7 +209,7 @@ impl Session {
}
"ls" => {
let dir = it.next().ok_or_else(|| "ls needs <dir>".to_string())?;
let fs = cubefs::CubeFs::new(self.store.clone());
let fs = cubefs::CubeFs::new(store.read_snapshot());
let entries = fs.readdir(dir).map_err(|e| format!("ls {dir}: {e:?}"))?;
if entries.is_empty() {
Ok(format!("ls {dir} -> (empty)"))
@@ -204,7 +220,7 @@ impl Session {
}
"stat" => {
let path = it.next().ok_or_else(|| "stat needs <path>".to_string())?;
let fs = cubefs::CubeFs::new(self.store.clone());
let fs = cubefs::CubeFs::new(store.read_snapshot());
let a = fs
.getattr(path)
.map_err(|e| format!("stat {path}: {e:?}"))?;
@@ -227,13 +243,8 @@ impl Session {
let transform = parse_transform(tf)
.ok_or_else(|| "unknown transform (none|gcm|chacha|xts)".to_string())?;
// Ensure key material exists at the Null-cube key cell.
if self.store.get_record(&kc).is_none() {
self.store.put_record(
kc,
&CubeHeader::new(),
b"demo-key-material-32-bytes-long!!",
);
if store.get_record(&kc).is_none() {
store.put_raw(kc, b"demo-key-material-32-bytes-long!!".to_vec());
}
let env = CubeEnv::new(
vec![KeySlot {
@@ -245,28 +256,29 @@ impl Session {
);
if cmd == "seal" {
let (h, body) = self
.store
let (h, body) = store
.get_record(&coord)
.ok_or_else(|| format!("seal: no record at {path}"))?;
env.put_encrypted(&mut self.store, coord, Selector::Slot(0), &body, h)
store
.with_mut(|s| env.put_encrypted(s, coord, Selector::Slot(0), &body, h))
.map_err(|e| format!("seal: {e:?}"))?;
// Log the re-written (encrypted) record to the WAL.
if let Some(v) = store.get_raw(&coord) {
store.log_put(coord, v);
}
Ok(format!("sealed {path} under key {} ({tf})", kc.pack_u32()))
} else {
let (_, envelope) = self
.store
let (_, envelope) = store
.get_record(&coord)
.ok_or_else(|| format!("open: no record at {path}"))?;
let pt = env
.open(&self.store, Selector::Slot(0), &envelope)
.open(&store.read_snapshot(), Selector::Slot(0), &envelope)
.map_err(|e| format!("open: {e:?}"))?;
let cell = CodeCell::from_record(coord, &CubeHeader::new(), &pt)
.ok_or_else(|| "open: decrypted body is not valid bytecode".to_string())?;
// The VM runs code located by coordinate, so to execute a
// sealed record we decrypt it back into a plaintext record,
// then run.
self.store.put_record(coord, &CubeHeader::new(), &pt);
let mut vm = Vm::new(self.store.clone());
store.put_raw(coord, pt);
let sn = store.read_snapshot();
let mut vm = Vm::new(sn);
let res = vm.run(cell.label);
Ok(format!(
"open+run {path} (key {}) => {res:?}",