Eliminating CAS Retry Collapse: Why NimbusKV 3.1 Moved to a Single Short Mutex
In theory, lock-free compare-and-swap (CAS) loops sound like the gold standard for concurrency. In practice, under high-thread write contention on a hot key, CAS creates a devastating retry collapse. Here is how and why we fixed it.
The Illusion of Optimistic CAS
In LiveDict and NimbusKV v3.0.1, the mutate() implementation followed the classic optimistic concurrency pattern: 1. Load current root pointer. 2. Run user mutation callback outside any lock. 3. Construct new HAMT path. 4. Try to atomically swap the root pointer using compare-and-swap. 5. If another thread swapped first, discard work and retry from Step 1.
Under 1 to 4 threads, this worked reasonably well. But when we tested under 16 concurrent threads hammering a hot key, throughput collapsed catastrophically from 439K ops/s down to 55K ops/s. 15 losing threads were constantly throwing their work away, burning CPU cycles in tight loops without making forward progress.
The Solution: One Short Mutex
In NimbusKV v3.1.0, we replaced the retry loop with a single deterministic mutex for writes: - Readers never touch the mutex; they remain 100% lock-free. - Each write acquires the lock once, performs path-copying, updates the root, and releases the lock. - User callbacks run exactly once per mutation.
The results from b06_write_path.py on a Linux sandbox were immediate: - set() throughput on standard Python 3.13 increased by +48% (from 328K to 485K ops/s). - set() throughput on free-threaded Python 3.14t increased by +157% (from 188K to 483K ops/s). - Hot-key contention remained stable instead of collapsing.