fix(914): thread-local ALL_OPS/VECTOR_CLOCK in cfg(test) so compaction tests don't race

Root cause was not the persist channel (the test-mode channel is unbounded
and its receiver is leaked, so sends never fail). It was that `ALL_OPS` and
`VECTOR_CLOCK` were process-wide `OnceLock` globals while `CRDT_STATE` was
already thread-local — so one test thread's `apply_compaction` would prune
another test thread's freshly-written ops out of the shared journal, and
the subsequent `all_ops_json()` read in `compaction_reduces_ops` would
return fewer than the 5 it had just written.

Mirror the pattern already used for `CRDT_STATE` and `SnapshotState`: in
`cfg(test)` use thread-local `OnceLock<Mutex<...>>`s for the op journal and
vector clock, accessed via new `all_ops_lock()` / `vector_clock_lock()`
helpers. Production code path is unchanged (still the global statics set
during `init()`).

Touches ops/read/snapshot call sites to go through the helpers. Note in
passing that this overlaps backlog story 518; that story is about the
production-side persist path, this is the cfg(test)-only journal-isolation
slice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Timmy
2026-05-12 16:09:38 +01:00
parent 379ff16d3e
commit 8421104645
6 changed files with 74 additions and 18 deletions
+2 -2
View File
@@ -404,7 +404,7 @@ pub fn apply_compaction(snapshot: Snapshot) -> bool {
// For this implementation, the snapshot state IS the full state — peers
// discard their old journal and replace it with the snapshot's ops.
// The op_manifest preserves attribution for the discarded ops.
if let Some(all_ops) = crdt_state::ALL_OPS.get()
if let Some(all_ops) = crdt_state::all_ops_lock()
&& let Ok(mut v) = all_ops.lock()
{
// Calculate ops to prune: those with seq < at_seq
@@ -428,7 +428,7 @@ pub fn apply_compaction(snapshot: Snapshot) -> bool {
*v = kept_ops;
// Rebuild vector clock from remaining ops.
if let Some(vc) = crdt_state::VECTOR_CLOCK.get()
if let Some(vc) = crdt_state::vector_clock_lock()
&& let Ok(mut clock) = vc.lock()
{
clock.clear();