Stamp owner_local_user on records written via prog/write and gate the
mutating paths (prog, write, del — both live and buffered txn) so a
session may only create or overwrite a record whose owner_local_user
matches its HELLO-declared identity.
Design (logical + expedient for the whole project):
- Owner is the durable record-level CubeHeader.owner_local_user field,
so enforcement is replay-safe and works across daemon restart.
- Enforcement is opt-in/non-breaking: gated only when the session has a
stamped identity AND the record has an owner. First write by an owner
claims an unowned coord; a session with no identity (tests, legacy)
writes freely.
- COMMIT re-checks owner on each buffered op before applying, so a
concurrent cross-owner commit between BEGIN and COMMIT is rejected
(txn is restored for retry, not silently dropped).
- seal/open (encrypted raw put/del) left ungated for now: their headers
are not owner-stamped yet — tracked as follow-up.
Verification:
- ./check quick: EXIT=0, fmt+clippy clean, 26 cubesys tests (added
owner_enforcement_blocks_cross_owner_overwrite,
owner_enforcement_allows_first_claim_and_same_owner,
for_tenant_carries_identity).
- Ad-hoc daemon verifier over real cube-server socket (LE framing):
cross-owner overwrite + delete rejected, same-owner + first-claim
allowed, no-HELLO legacy writes allowed. ALL PASS.
Two correctness bugs found via ad-hoc daemon verification (T5 was
compile-verified only before):
1. decode_wal dropped WalOp::Txn entries on replay: the txn encoder emits
{"seq","op":"txn","batch"} with NO c/z/y/x fields, but decode_wal
read c/z/y/x unconditionally -> field_u8("c") returned Err -> the
whole entry was skipped. Committed transactions silently vanished on
restart. Fix: branch on op=='txn' before the c/z/y/x extraction.
2. commit was not synchronously durable: append_txn only buffered to the
WAL pending buffer; fsync happened on the 25ms group thread. A
clean stop within that window lost the commit. Fix: commit_txn now
calls wal.flush_pending() (fsync) before returning, so COMMIT is
durable on return -- a real transaction boundary.
Adds unit test commit_replays_from_wal_without_checkpoint (would have
failed before fix 1). Ad-hoc verifier exercises all 3 changed paths on
the live cube-server socket.
ConcurrentStore.inner is now Arc<RwLock<CubeStore>>: all read paths take the
read side, all mutations + checkpoint take the write side. Readers no longer
exclude each other and overlap an active writer (verified by
concurrent_reads_dont_block_on_writer + cube-bench Task 4 section). WAL,
checkpoint, and coordinate encoding are untouched, so durability/replay is
unchanged (.check green).
Honest finding recorded in docs/task4-reader-writer-sharding.md: on this 8-core
host std RwLock removes reader-vs-reader exclusion (correct) but shows no
wall-clock speedup for short reads (cache-line bounce on one shared lock). Real
read-throughput scaling would need sharded/lock-free storage, left as a
follow-up decision rather than invented.
open() derived the delta path as "${db_path}.delta" (e.g. "db3.json.delta")
while checkpoint_store() writes via db_path.with_extension("delta")
(e.g. "db3.delta"). On reopen, load_base_plus_delta therefore read a
never-written path and silently skipped the delta, so post-checkpoint
changes were lost. Use db_p.with_extension("delta") in both places.
Also drop a no-op cp_seq.max(0) (u64 >= 0 always) to clear the clippy
-W clippy::unnecessary_min_or_max lint.
Verified: ./check all green; incremental_checkpoint_delta_model,
durable_checkpoint_and_replay, wal_recovery_after_crash pass in isolation.
- 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.