Data-SortedSet-Shared
view release on metacpan or search on metacpan
NAME
Data::SortedSet::Shared - shared-memory sorted set (ZSET) for Linux
SYNOPSIS
use Data::SortedSet::Shared;
# up to 1M members, anonymous shared mapping
my $z = Data::SortedSet::Shared->new(undef, 1_000_000);
$z->add(42, 1500); # member 42 with score 1500
$z->add(7, 1500); # ties broken by member id
$z->incr(42, 50); # 42 -> 1550 (returns the new score)
my @top = $z->rev_range_by_rank(0, 9); # top 10 members (highest score)
my $rank = $z->rank(42); # 0-based rank (lowest score = 0)
my $score = $z->score(42);
my @near = $z->range_by_score(1400, 1600); # members scored in [1400, 1600]
my ($m, $s) = $z->pop_min; # remove + return the lowest
$z->each(sub { my ($member, $score) = @_; ... }); # in score order
DESCRIPTION
An ordered set in shared memory, in the spirit of a Redis sorted set:
each member is a 64-bit integer carrying a double score, and members are
kept in score order. It is backed by an order-statistics B+tree -- so
"rank", range, and "pop" are O(log n) and ranges scan sequentially
through doubly-linked leaves -- paired with a member-to-score hash
index, so "score" and "exists" are O(1).
The total order is (score, member): members with equal scores are
ordered by member id, which gives a well-defined rank and a
deterministic "pop_min"/"pop_max".
Multiple processes can map the same set and read and write it
concurrently; access is serialized by a write-preferring futex rwlock
that recovers automatically if a lock holder dies (see "CRASH SAFETY").
Members are 64-bit integers. For string-keyed sets, see "String-keyed
sets" and Data::SortedSet::Shared::Strings (bundled). Scores must not be
NaN. Linux-only. Requires 64-bit Perl.
METHODS
Constructors
my $z = Data::SortedSet::Shared->new($path, $max [, $mode]);
my $z = Data::SortedSet::Shared->new(undef, $max); # anonymous
my $z = Data::SortedSet::Shared->new_memfd($name, $max);
my $z = Data::SortedSet::Shared->new_from_fd($fd);
$path is the backing file ("undef" for an anonymous mapping); $max is
the maximum number of members. When reopening an existing file or memfd,
the stored header wins and the caller's $max is ignored. Backing files
are created with mode 0600 by default; pass an octal $mode (e.g. 0660)
to opt into cross-user sharing. The mode applies only when the file is
created (it is ignored when attaching an existing file) and is subject
to umask. "new_memfd" creates a Linux memfd (transferable via its
"memfd" descriptor); "new_from_fd" reopens one in another process.
String-keyed sets
my $z = Data::SortedSet::Shared->new_strings(max => 1_000_000);
$z->add("alice", 1500);
my @top = $z->rev_range_by_rank(0, 9); # ("alice", ...)
"new_strings" returns a Data::SortedSet::Shared::Strings -- the same API
as this class but with string members. Keys are interned to dense ids
via Data::Intern::Shared (a prerequisite of this distribution), so the
set is still shared across processes by id. Ties among equal scores
break by interning id, not lexicographically. See
Data::SortedSet::Shared::Strings for the full options ("set"/"keys"
backing paths, "max_keys", "arena", "mode"), and the separate "wrap"
constructor that adopts two existing objects.
Mutators
$z->add($member, $score); # 1 new, 0 existing (score updated), undef if full
$z->incr($member, $delta); # add to the score (creating at $delta); returns new score
$z->remove($member); # true if removed, false if absent
my $n = $z->add_many([ [$m1,$s1], [$m2,$s2], ... ]); # bulk; returns count of new
$z->clear;
"add" inserts a new member or updates an existing member's score,
returning 1 or 0 respectively, or "undef" if the pool is full and the
member is new. $score may be any finite or infinite value but not NaN
(croaks). "incr" creates an absent member at $delta (like Redis ZINCRBY)
and croaks if the result would be NaN, or if the pool is full and the
member is new.
"add_many" applies a whole batch under a single lock; each row is an
"[member, score]" arrayref, malformed or NaN-scored rows are skipped,
and it stops at $max. It returns the number of members newly inserted,
which can be fewer than the number of new rows if the pool fills
mid-batch.
Lookup and count
$z->score($member); # the score, or undef if absent
$z->exists($member);
$z->count; # number of members
$z->rank($member); # 0-based rank (lowest score = 0), or undef
$z->rev_rank($member); # rank from the top, or undef
$z->count_in_score($min, $max); # members with score in [min, max] (inclusive)
Rank and range
$z->at_rank($r); # member at rank $r (negative counts from the end), or undef
my @m = $z->range_by_rank($start, $stop); # members in [start .. stop] by rank
my @m = $z->rev_range_by_rank(0, 9); # top 10 (highest scores first)
my @m = $z->range_by_score($min, $max, %opts); # members scored in [min, max], ascending
my @m = $z->rev_range_by_score($max, $min, %opts);
Rank indices are 0-based and may be negative (counting from the end,
like Perl slices); "range_by_*" bounds are inclusive. "range_by_score" /
"rev_range_by_score" accept "limit => $n" and "offset => $k" (a negative
"offset" is treated as 0). All range and rank methods return members;
pass "withscores => 1" for a flat "(member, score, ...)" list instead.
Pop and peek
my ($member, $score) = $z->pop_min; # remove + return the lowest, or () if empty
my ($member, $score) = $z->pop_max;
my ($member, $score) = $z->peek_min; # without removing
my ($member, $score) = $z->peek_max;
Iteration
$z->each(sub { my ($member, $score) = @_; ... });
"each" snapshots all members under the read lock, then invokes the
callback once per member in score order after the lock is released, so
the callback may safely call back into the set.
Introspection and lifecycle
$z->count; $z->max_entries; $z->stats; # see STATS
$z->path; $z->memfd; $z->sync; $z->unlink; # or Class->unlink($path)
$z->eventfd; $z->fileno; $z->notify; $z->eventfd_consume;
"sync" flushes the mapping to its backing store and "unlink" removes the
backing file (also callable as "Class->unlink($path)"). "path" returns
the backing file path ("undef" for an anonymous or memfd-backed set) and
"memfd" returns the descriptor of a "new_memfd" set (-1 otherwise). The
eventfd methods let another process wait for updates: "eventfd" lazily
creates an eventfd and returns its descriptor (croaks on failure;
calling it again returns the same fd), "fileno" returns the current
eventfd descriptor or -1, "notify" writes a wakeup (returning false if
no eventfd is attached), and "eventfd_consume" reads and resets the
counter, returning it as an integer or "undef" when nothing is pending.
SHARING ACROSS PROCESSES
The set lives in a shared mapping, so several processes operate on the
same data with no serialization layer in between. There are three ways
to share it:
* A backing file -- every process calls "new($path, $max)" on the same
( run in 0.675 second using v1.01-cache-2.11-cpan-9581c071862 )