This design record explains why the volatile (in-memory) dictionaries are built the way they are
for concurrency, the invariants that make them correct, and how those invariants are tested. The
what — the structures themselves — is in
architecture/in-memory-dictionaries.md; this document is
the rationale and the verification story. Notation follows docs/notation.md.
Goal. Every mutable in-memory backend must give wait-free reads (a lookup never blocks, never spins, and completes in a bounded number of steps regardless of concurrent writers) and lock-free writes (a writer never holds a global mutation lock; system-wide progress is guaranteed). This matters because these dictionaries sit on the hot path of a fuzzy transducer that fans a single query into thousands of concurrent node traversals — a reader-side lock there would serialize the whole search.
Non-goals. Linearizable multi-key transactions are out of scope: each insert / remove /
contains is individually linearizable, but there is no cross-term atomic batch. Durability is also
out of scope here — these backends are volatile by definition; the durable story is the
persistent ARTrie.
RwLock?The obvious design — wrap the structure in a parking_lot::RwLock — fails the goal on the read side:
readers contend on the lock's atomics and, under a writer, block entirely. For a workload that is
overwhelmingly reads issued in parallel, that is the wrong trade. The chosen design instead makes the
published state immutable, so readers need no lock at all: they load an Arc snapshot and walk it,
and the snapshot cannot change under them because writers publish new immutable state rather than
mutating the old.
The
default = ["parking_lot"]feature still pulls inparking_lot, but for the dynamic backends' internal bookkeeping, not as a reader-visible global lock on the trie. The lock-free claim is about the read/publish path.
The rationale for the split (detailed in the architecture doc) comes down to edit locality:
DynamicDawg, …Char, …U64) — a DAWG edit touches only one
root-to-terminal route, so the writer copies that route, shares unchanged branches, and publishes
one new root. A reader that retained the earlier root remains completely undisturbed.SuffixAutomaton, Scdawg, PathMap) — an edit here is
not path-local (a suffix-automaton extend can clone-split a state and rewire suffix links
graph-wide), so the simplest correct linearization point is a single CAS on the root pointer after
building the new revision. PathMap makes this especially cheap because it is itself a persistent
trie: cloning it is an $O(1)$ structural share, not a deep copy.The design rests on four invariants, each a direct consequence of publish-immutable-state-by-CAS:
insert / remove / contains has a single atomic point
at which it takes effect (the CAS that publishes the edit, or the load that reads the current
pointer), so concurrent operations are equivalent to some sequential order.CasBackoff, src/nonblocking),
so no update is silently dropped. BijectiveMap additionally runs a rollback step to keep its
term↔value bijection consistent if a reverse-map race is lost.Arc referencing it drops — reference counting is the reclamation scheme, sound because
replaced data is immutable once unpublished. No epoch machinery is needed here (unlike the
persistent overlay, which manages raw pointers).Lock-free writers use CasBackoff (src/nonblocking), a bounded exponential-backoff helper around
the compare-and-swap loop. It caps spinning so a heavily contended cell degrades gracefully rather
than live-locking. Because a retry re-reads the current published state and rebuilds against it,
invariant 3 (no lost writes) holds no matter how many writers contend.
The model is checked, not asserted:
tests/query_start_snapshot_correspondence.rs
retains one byte, character, or u64 root while inserts, removals, value replacements, and
compaction publish newer revisions, then asserts the retained traversal remains exact.tests/volatile_lockfree_concurrency.rs
drives DynamicDawg, SuffixAutomaton, Scdawg, PathMap, and BijectiveMap under concurrent
readers and writers, asserting that reads always observe a consistent dictionary and that no write
is lost.tests/dictionary_law_correspondence.rs
and tests/dynamic_dawg_mutation_correspondence.rs
pin the observable semantics (insert-then-contains, remove-then-not-contains, idempotence) that the
concurrent implementation must preserve.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |