Data-HashMap-Shared

 view release on metacpan or  search on metacpan

lib/Data/HashMap/Shared.pm  view on Meta::CPAN

seal flag in the header, and flushes it to disk (C<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.

C<new_readonly> opens an already-frozen file with C<O_RDONLY> and maps it
C<PROT_READ>. Because a sealed map is immutable and can have no writers, queries
take B<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: C<get>,
C<exists>, C<get_with_ttl>, C<get_multi>, C<keys>, C<values>, C<items>,
C<to_hash>, C<each>, and cursors (C<cursor> and C<cursor_next>). Every mutator
croaks (including the integer counters C<incr>/C<decr>/C<max>/C<min>, which on a
writable map update in place under the read lock). C<sync> is a silent no-op.
C<frozen> and C<readonly> report the state, and C<stats> gains matching
C<frozen> and C<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 (C<new>,
C<new_from_fd>) is refused -- open it with C<new_readonly> instead -- and
C<new_readonly> refuses a file that has not been frozen (its lock-free readers
must never race a live writer). C<new_readonly> is for a single backing file,
not a sharded set.

B<Portability>: a frozen file is a raw memory image. Read it back on the B<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
B<copying> the file; do not serve it over NFS or another network filesystem
while another host has it mapped.

=head2 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 C<FUTEX_WAIT> timeout, waiters
C<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 C<pthread_atfork>).  A reader's B<entire> contribution to
the lock is the C<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-C<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 L</"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.  C<stat_recoveries> in C<stats> counts
every recovery event.

Recovery uses C<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 B<and> the kernel to cycle
through the entire PID space back to that exact number within the ~2-second
recovery window B<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).

B<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
C<clear> after detecting a stale lock recovery is recommended for
safety-critical applications.

=head2 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.

=head1 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 C<perl -Mblib bench/vs.pl 25000> to reproduce.

B<Integer key -> integer value> (Shared::II):

              BerkeleyDB   LMDB   Shared::II
    INSERT          31       46         184
    LOOKUP          35       40         383
    INCREMENT       16       18         165

B<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

B<String key -> string value, long> (~50-100B, Shared::SS):

              BerkeleyDB   LMDB   SharedMem   Shared::SS
    INSERT        25         37        61          133
    LOOKUP        30         33       125          229

B<LRU cache lookup> (25K entries, lock-free clock eviction):



( run in 1.798 second using v1.01-cache-2.11-cpan-14f38c9f855 )