Data-HashMap-Shared
view release on metacpan or search on metacpan
shipped and served read-only. It takes the write lock (so no mutation is
in flight), sets a seal flag in the header, and flushes it to disk
("msync"). Afterwards every mutator on that handle croaks and the handle
itself becomes read-only. A sharded map seals every shard file. Freezing
is one-way; there is no unfreeze.
"new_readonly" opens an already-frozen file with "O_RDONLY" and maps it
"PROT_READ". Because a sealed map is immutable and can have no writers,
queries take no lock at all -- no reader-slot bookkeeping, no LRU clock
bit, no lazy TTL cleanup -- and never write the mapping. A read-only view
therefore works from a read-only file or filesystem, and any number of
processes can share one frozen file at once. All queries and full
iteration are supported: "get", "exists", "get_with_ttl", "get_multi",
"keys", "values", "items", "to_hash", "each", and cursors ("cursor" and
"cursor_next"). Every mutator croaks (including the integer counters
"incr"/"decr"/"max"/"min", which on a writable map update in place under
the read lock). "sync" is a silent no-op. "frozen" and "readonly" report
the state, and "stats" gains matching "frozen" and "readonly" keys.
The seal reuses a previously reserved header byte, so the on-disk format
and version are unchanged: a file written by an older release is simply
not frozen (the byte reads 0) and opens read-write exactly as before.
The two modes never mix: opening a frozen file read-write ("new",
"new_from_fd") is refused -- open it with "new_readonly" instead -- and
"new_readonly" refuses a file that has not been frozen (its lock-free
readers must never race a live writer). "new_readonly" is for a single
backing file, not a sharded set.
Portability: a frozen file is a raw memory image. Read it back on the same
architecture that wrote it (same word size and endianness; the native
magic and variant id reject a mismatched or wrong-variant file at attach
time). Ship it by copying the file; do not serve it over NFS or another
network filesystem while another host has it mapped.
Crash Safety
If a process dies (e.g., SIGKILL, OOM kill) while holding the write lock,
other processes detect the stale lock within 2 seconds and automatically
recover. The writer's PID is encoded in the rwlock word itself (single
atomic CAS, no crash window). On "FUTEX_WAIT" timeout, waiters "kill($pid,
0)" the holder and CAS-release the lock if it's dead.
Reader-side recovery uses a 1024-slot table in the shared mmap (one slot
per process, claimed lazily on first lock; fork()'d children claim a fresh
slot via "pthread_atfork"). A reader's entire contribution to the lock is
the "rdepth" word in its own slot -- there is no shared reader counter --
so a dead reader is neutralised by clearing that one slot's PID, and a
draining writer does so unconditionally as it scans. Because no orphaned
count can exist, there is no quiescent force-reset: a worker killed
mid-"incr_by" cannot pin the lock at all. An occupancy bitmap (one bit per
slot, published before the slot can hold a lock) lets the writer visit
only occupied slots rather than all 1024. Beyond 1024 simultaneous handles
per map, a handle that cannot claim a slot proceeds "slotless"; see
"Reader-slot exhaustion" for the one case that recovery cannot cover.
The same path validates and rebuilds the LRU doubly-linked list if a dead
writer left it inconsistent. "stat_recoveries" in "stats" counts every
recovery event.
Recovery uses "kill($pid, 0)" for liveness, which cannot distinguish a
reused PID from the original. Hitting a false "alive" requires a process
to die in the brief window it holds a read lock and the kernel to cycle
through the entire PID space back to that exact number within the
~2-second recovery window and hand it to a long-lived process -- i.e. a
runaway fork storm. Even then the effect is bounded: writers stall until
the recycled process exits; reads are unaffected and no data is corrupted.
Writer-crash recovery is immune (the writer PID lives in the lock word and
is reclaimed independently of the slot table).
Limitation: PID-based recovery assumes all processes share the same PID
namespace. Cross-container sharing (different PID namespaces) is not
supported.
After recovery from a mid-mutation crash, the map data may be partially
inconsistent (e.g., one entry was being updated when the writer died). Map
structure (locks, LRU, free lists, counters) is restored, but the specific
entry being mutated may have stale or partial bytes. Calling "clear" after
detecting a stale lock recovery is recommended for safety-critical
applications.
Reader-slot exhaustion
Dead-process recovery attributes a crashed lock holder's contribution
through its reader-slot. The slot table holds 1024 entries (one per
concurrent reader process). If more than that many reader processes share
one mapping at once, a reader that cannot claim a slot proceeds "slotless"
-- it still takes the read lock but leaves no per-process record. If such
a slotless reader is then killed while holding the read lock, its share of
the lock cannot be attributed to a dead process, so writer recovery cannot
reclaim it and writers may block until the mapping is recreated. Reaching
this needs more than 1024 concurrent reader processes on one mapping plus
a crash in the brief read-lock window; the dead-process slot reclaim keeps
the table from filling with stale entries, so in practice it is very
unlikely.
BENCHMARKS
Throughput versus other shared-memory / on-disk solutions, 25K entries,
single process, Linux x86_64. All values in M ops/s (higher is better).
Run "perl -Mblib bench/vs.pl 25000" to reproduce.
Integer key - integer value> (Shared::II):
BerkeleyDB LMDB Shared::II
INSERT 31 46 184
LOOKUP 35 40 383
INCREMENT 16 18 165
String key - string value, short> (inline <= 7B, Shared::SS):
FastMmap BerkeleyDB LMDB SharedMem Shared::SS
INSERT 11 26 40 62 130
LOOKUP 10 32 34 146 213
DELETE 14 18 -- 32 68
String key - string value, long> (~50-100B, Shared::SS):
BerkeleyDB LMDB SharedMem Shared::SS
INSERT 25 37 61 133
LOOKUP 30 33 125 229
LRU cache lookup (25K entries, lock-free clock eviction):
( run in 0.946 second using v1.01-cache-2.11-cpan-14f38c9f855 )