NAME
Data::HashMap::Shared::Cookbook - Practical recipes for shared-memory maps
DESCRIPTION
Copy-paste patterns for common cross-process tasks with Data::HashMap::Shared: counters, caches, rate limiting, liveness, work queues, and atomic state. Each recipe is a complete, self-contained sketch; runnable versions of several live under eg/ in the distribution.
Pick a variant by key/value type (see "CHOOSING A VARIANT") and replace the shm_xx_* keyword prefix accordingly (shm_ii_, shm_ss_, shm_si_, ...).
RECIPES
Shared request/error counters
Pre-forked workers tallying into one map. incr/incr_by are atomic under a read lock, so no update is lost.
use Data::HashMap::Shared::SI; # string metric name -> int64 count
my $stats = Data::HashMap::Shared::SI->new('/run/myapp/stats.shm', 1000);
# in each worker, per request:
shm_si_incr $stats, 'requests';
shm_si_incr_by $stats, 'bytes_out', $n;
shm_si_incr $stats, 'errors' if $failed;
# anytime, from any process:
my %snapshot = %{ $stats->to_hash };
For write-heavy counters spread the load with new_sharded (see eg/sharded_counter.pl).
High-water marks and leaderboards
max/min store max($current, $desired) / min(...) atomically under a single lock, so concurrent updates never clobber each other or lose a higher score. A missing key is inserted as the given value.
use Data::HashMap::Shared::SI; # player -> high score
my $board = Data::HashMap::Shared::SI->new('/run/game/board.shm', 10_000);
shm_si_max $board, $player, $score; # keeps only the best, race-free
shm_si_min $board, 'min_latency_us', $sample;
my $scores = $board->to_hash;
my @top = (sort { $scores->{$b} <=> $scores->{$a} } keys %$scores)[0 .. 9];
Full example: eg/leaderboard.pl.
Cache-aside memoization (compute once)
get_or_set stores a key's value once; racing callers all receive the same stored value, so an expensive computation is shared across the fleet.
use Data::HashMap::Shared::IS; # int id -> string result
my $cache = Data::HashMap::Shared::IS->new('/run/myapp/memo.shm', 100_000);
sub lookup {
my $id = shift;
my $hit = shm_is_get $cache, $id;
return $hit if defined $hit; # fast path
return shm_is_get_or_set $cache, $id, compute($id); # first writer wins
}
Full example: eg/memoize.pl.
LRU cache with bounded memory
Pass $max_size to cap live entries; the least-recently-used entry is evicted on insert (clock/second-chance; reads stay lock-free).
use Data::HashMap::Shared::SS;
# max_entries=1_000_000 table, evict once 100_000 entries are live:
my $cache = Data::HashMap::Shared::SS->new('/run/myapp/cache.shm', 1_000_000, 100_000);
shm_ss_put $cache, $key, $value; # auto-evicts LRU when full
my $v = shm_ss_get $cache, $key; # also refreshes recency (clock bit)
my $evicted = $cache->stats->{evictions};
For large values on a small table, size the string arena explicitly so a few big entries do not starve it: ->new($path, $max_entries, 0, 0, 0, $arena_cap).
Per-key TTL and dedup / idempotency
A default TTL expires entries lazily on access; add inserts only if absent. Together they make a "process each id once within a window" guard.
use Data::HashMap::Shared::IS; # event id -> marker, 300s TTL
my $seen = Data::HashMap::Shared::IS->new('/run/myapp/seen.shm', 1_000_000, 0, 300);
if ($seen->add($event_id, '1')) { # true only the first time (then TTL'd)
handle($event_id);
} # else: already processed recently, skip
put_ttl/set_ttl set a per-key TTL; persist makes a key permanent; ttl_remaining reports seconds left. TTL is measured on a monotonic clock.
Worker heartbeat / liveness registry
Each worker refreshes a TTL'd heartbeat; a dead worker stops refreshing and its entry expires, so a supervisor sees only live workers.
use Data::HashMap::Shared::IS; # pid -> status, 2s TTL
my $reg = Data::HashMap::Shared::IS->new('/run/myapp/live.shm', 4096, 0, 2);
# worker loop:
shm_is_put $reg, $$, 'ok'; # every second; resets the TTL
# supervisor:
$reg->flush_expired; # drop stale heartbeats
my @live = $reg->keys;
Full example: eg/heartbeat.pl.
Token-bucket rate limiting
A counter per subject with a TTL window; the first hit creates it (TTL applies), subsequent hits increment, and the key expires to reset the window.
use Data::HashMap::Shared::SI; # subject -> hits, 60s window
my $rl = Data::HashMap::Shared::SI->new('/run/myapp/rl.shm', 100_000, 0, 60);
sub allow {
my $who = shift;
my $n = shm_si_incr $rl, $who; # auto-creates at 1 with the default TTL
return $n <= 100; # cap per window
}
Full example: eg/rate_limiter.pl.
Atomic state machine (compare-and-swap)
cas swaps only if the current value matches, so processes can drive a shared state machine without a lock.
use Data::HashMap::Shared::SI; # job -> state code (0=idle 1=running 2=done)
my $jobs = Data::HashMap::Shared::SI->new('/run/myapp/jobs.shm', 100_000);
# claim a job: idle(0) -> running(1), exactly one winner
if (shm_si_cas $jobs, $job, 0, 1) {
run($job);
shm_si_cas $jobs, $job, 1, 2; # running -> done
}
cas_take atomically removes a key only if it matches (handy for one-shot claims); see also the work-queue pattern in eg/work_queue.pl.
CHOOSING A VARIANT
Variants are <key><value> where I16/I32/I are signed 16/32/64-bit integers and S is a byte string. Integer variants store values inline (no arena, fastest); string sides use the shared arena.
II int64 -> int64 counters, ids-to-ids
I16 int16 -> int16 compact enums / small counters
I32 int32 -> int32
IS int64 -> string id -> blob/json (memoization)
I16S int16 -> string
I32S int32 -> string
SI string -> int64 named counters, scores, rate limits
SI16 string -> int16
SI32 string -> int32
SS string -> string config, sessions, generic caches
Use the narrowest integer width that fits your range (values wrap at the variant's width; see "Integer Range and Wrapping" in Data::HashMap::Shared). The incr/decr/incr_by/max/min counter ops exist only on integer-value variants.
SIZING
->new($path, $max_entries, $max_size, $ttl, $lru_skip, $arena_cap)
$max_entries — table capacity (it grows/shrinks elastically up to this). Round up for your peak live-key count.
$max_size — LRU cap (0 = no eviction). Set to bound memory; entries beyond it evict least-recently-used.
$ttl — default per-entry TTL in seconds (0 = none). Set for caches / windows; combine with
$max_sizefor a bounded TTL cache.$lru_skip — LRU promotion-skip percentage (0-99, default 0). Leave at 0; raise only if profiling shows write-lock contention from LRU promotions on a Zipfian workload. See "Constructor" in Data::HashMap::Shared.
$arena_cap — string-storage bytes (0 =
max(max_entries*128, 4096)). Set explicitly when a few large string keys/values would otherwise exhaust the default; integer-only variants ignore it.shards (
new_sharded($prefix, $shards, ...)) — independent maps with independent locks for write-heavy workloads; per-key ops route automatically, and the sizing args above are per shard.
SEE ALSO
Data::HashMap::Shared for the full API; the eg/ directory for runnable versions of these recipes.
AUTHOR
vividsnow
LICENSE
Same terms as Perl itself.