NAME
    Data::Reservoir::Shared - shared-memory reservoir sampler (uniform stream
    sample)

SYNOPSIS
        use Data::Reservoir::Shared;

        # keep a uniform random sample of 100 items from an unbounded stream
        my $rsv = Data::Reservoir::Shared->new(undef, 100);

        $rsv->add($_) for @stream;      # feed items; each is kept or discarded

        my @sample = $rsv->sample;      # up to 100 items, uniformly sampled
        $rsv->count;                    # how many are held (min of seen, 100)
        $rsv->seen;                     # total items observed

        # share the reservoir across processes via a backing file
        my $shared = Data::Reservoir::Shared->new("/tmp/sample.rsv", 100);

        # weighted sampling: keep items with probability proportional to weight
        my $wrsv = Data::Reservoir::Shared->new_weighted(undef, 100);
        $wrsv->add($event, $weight) for @stream;   # heavier items kept more often

DESCRIPTION
    A reservoir sampler in shared memory: it keeps a uniform random sample of
    "k" items drawn from a stream of unknown or unbounded length, in fixed
    memory, using Algorithm R. Feed it items one at a time with "add"; while
    fewer than "k" items have been seen every item is kept, and after that the
    i-th item replaces a uniformly random slot with probability "k/i". At any
    point the "k" retained items are a uniform random sample of everything
    seen so far -- the standard way to sample a log stream, sample events for
    telemetry, or take a fair sample of a data set too large to hold.

    Because the reservoir lives in a shared mapping, several processes feed
    and read one sample: any process that opens the same backing file,
    inherits the anonymous mapping across "fork", or reopens a passed memfd
    contributes to and reads the same reservoir. The sampling RNG (a
    xorshift64) lives in the shared header and is advanced under the write
    lock, so concurrent producers sample one consistent reservoir. A
    write-preferring futex rwlock with dead-process recovery guards mutation.

    Items are stored inline, truncated to "item_size" bytes (default 256): an
    item longer than "item_size" keeps only its first "item_size" bytes.
    Memory is about "k * S" bytes for the slots, where the per-slot stride "S"
    rounds "8 + item_size" up to a multiple of 8, plus a fixed header. Items
    are handled by their byte content; wide-character strings (any codepoint
    above 255) cause a "Wide character" croak -- encode to bytes first.
    Linux-only. Requires 64-bit Perl.

  Weighted sampling (A-Res)
    A reservoir created with "new_weighted" keeps a weighted random sample:
    each item carries a positive weight and is retained with probability
    proportional to its weight rather than uniformly. This is the
    Efraimidis-Spirakis A-Res algorithm -- each observed item is assigned a
    key "u ** (1/weight)" (with "u" uniform in "(0,1]") and the "k" items with
    the largest keys are kept, tracked in a shared min-heap so a heavier
    arrival can evict the current lightest member. With a single slot the
    probability of keeping item "i" is exactly "w_i / sum(w)". Feed weighted
    items with "$rsv->add($item, $weight)" (the weight must be a finite number
    greater than zero); everything else -- "sample", "count", "get", "clear",
    and cross-process sharing -- works identically. A weighted reservoir
    stores an extra "k * 16" bytes for the heap and records its mode in the
    header, so a reopened segment stays weighted.

METHODS
  Constructors
        my $rsv = Data::Reservoir::Shared->new($path, $k, $item_size);
        my $rsv = Data::Reservoir::Shared->new(undef, $k);              # anonymous, item_size 256
        my $rsv = Data::Reservoir::Shared->new_memfd($name, $k, $item_size);
        my $rsv = Data::Reservoir::Shared->new_from_fd($fd);

        # weighted (A-Res) reservoir -- same arguments, weighted sampling
        my $rsv = Data::Reservoir::Shared->new_weighted($path, $k, $item_size);
        my $rsv = Data::Reservoir::Shared->new_weighted_memfd($name, $k, $item_size);

    $k is the reservoir size (the number of items to retain, at least 1).
    $item_size is the maximum bytes stored per item (default 256; items are
    truncated to it). "new" and "new_memfd" croak on a size below 1 or an
    out-of-range $item_size. When reopening an existing file or memfd the
    stored geometry wins and the caller's arguments do not resize it -- but
    they are still range-checked, so an out-of-range value croaks. An optional
    file mode may be passed as the last argument to "new" (e.g. 0660) for
    cross-user sharing; it defaults to 0600 (owner-only).

  Sampling
        my $kept  = $rsv->add($item);            # uniform: 1 if now stored, 0 if discarded
        my $kept  = $rsv->add($item, $weight);    # weighted reservoir: weight must be > 0
        my $n     = $rsv->add_many(\@items);              # uniform: array ref of items
        my $n     = $rsv->add_many([[$item,$w], ...]);     # weighted: [item, weight] pairs
        my @items = $rsv->sample;            # current sample (a list, up to k items)
        my $it    = $rsv->get($i);           # the i-th retained item (0-based), or undef
        $rsv->clear;                         # empty the reservoir (seen resets to 0)

    "add" feeds one item and returns 1 if it is now stored in the reservoir or
    0 if it was discarded. On a weighted reservoir "add" takes a second
    argument, the item's weight (a finite number greater than zero -- a
    missing, zero, negative, infinite, or NaN weight croaks). "add_many" feeds
    an array reference under a single write lock, returning how many of the
    batch were stored (an item that replaces an earlier sample counts as
    stored; use "count" for the number currently retained); for a weighted
    reservoir each element must be an "[$item, $weight]" pair. "sample"
    returns the retained items as a list (order is not meaningful); "get"
    returns a single retained item by index. "clear" empties the reservoir and
    resets the seen counter.

  Introspection and RNG
        $rsv->count;        # number of items currently held: min(seen, k)
        $rsv->seen;         # total items observed
        $rsv->capacity;     # k
        $rsv->item_size;    # max bytes per item
        $rsv->is_weighted;  # true for a weighted (A-Res) reservoir
        $rsv->seed($n);     # set the RNG state (for reproducible sampling in tests)
        $rsv->stats;        # { size, item_size, count, seen, ops, mmap_size, weighted }

    "seed" sets the shared RNG state to a fixed value, making subsequent
    sampling deterministic -- useful for reproducible tests. By default the
    RNG is seeded from process and clock entropy at creation, so different
    runs draw different samples.

  Lifecycle
        $rsv->path; $rsv->memfd; $rsv->sync; $rsv->unlink;

    "sync" flushes the mapping to its backing store (a no-op for anonymous and
    memfd reservoirs); "unlink" removes the backing file (also callable as
    "Class->unlink($path)"); "path" returns the backing path ("undef" for
    anonymous, memfd, or fd-reopened reservoirs) and "memfd" the backing
    descriptor.

SHARING ACROSS PROCESSES
    The reservoir lives in a shared mapping, shared the same three ways as the
    rest of the family: a backing file, an anonymous mapping inherited across
    "fork", or a memfd passed to an unrelated process and reopened with
    new_from_fd($fd). The descriptor you pass is duplicated
    ("F_DUPFD_CLOEXEC"), so it stays yours to close and closing it does not
    disturb the handle. Every process's "add" feeds the one shared reservoir,
    and the shared RNG keeps the sampling probabilities consistent across
    producers.

SECURITY
    Backing files are created with mode 0600 (owner-only) by default; pass an
    explicit octal mode (e.g. 0660) as the last argument to "new" for
    cross-user sharing. The file is opened with "O_NOFOLLOW" and "O_EXCL", and
    the header is validated on attach. Any process granted write access is
    trusted not to corrupt the mapping.

CRASH SAFETY
    Mutation is guarded by a futex-based write-preferring rwlock with
    PID-encoded ownership and dead-owner recovery. Each "add" is a short
    bounded update, so a crash leaves the reservoir consistent up to the last
    completed operation. Limitation: PID reuse is not detected (very unlikely
    in practice).

    Reader-slot exhaustion (slotless readers): 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.

    An interrupted create is recovered too. A creator killed after the backing
    file is sized but before its header is committed leaves a full-size,
    all-zero file. "new" re-initializes such a file automatically, but only
    when it is exactly the size the requested geometry needs, is owned by your
    effective uid, and is still entirely zero -- a file holding data is never
    re-initialized. If the creator got as far as writing part of the header,
    the file cannot be told apart from a corrupt one and "new" croaks with
    "incomplete reservoir file left by an interrupted create; remove it and
    retry". A file left behind by an interrupted create never held data, so
    removing it is safe -- but a file whose header was corrupted after the
    fact reaches the same croak, so confirm it is an abandoned create before
    deleting anything you care about.

SEE ALSO
    Data::HyperLogLog::Shared (cardinality estimation),
    Data::CountMinSketch::Shared (frequency estimation), and the rest of the
    "Data::*::Shared" family.

AUTHOR
    vividsnow

LICENSE
    This is free software; you can redistribute it and/or modify it under the
    same terms as Perl itself.

