NAME
    Data::Log::Shared - Append-only shared-memory log (WAL) for Linux

SYNOPSIS
        use Data::Log::Shared;

        my $log = Data::Log::Shared->new(undef, 1_000_000);
        my $off = $log->append("first entry");
        $log->append("second entry");

        # replay from beginning (each_entry skips abandoned slots silently)
        $log->each_entry(sub {
            my ($data, $offset) = @_;
            say "offset=$offset: $data";
        });

        # or manually -- guard with `defined $data` since a slot from a
        # crashed writer is reported as (undef, $next)
        my $pos = 0;
        while (my ($data, $next) = $log->read_entry($pos)) {
            say "offset=$pos: $data" if defined $data;
            $pos = $next;
        }

        # tail: block until new entries
        my $count = $log->entry_count;
        $log->wait_for($count, 5.0);

        # file-backed / memfd
        $log = Data::Log::Shared->new('/tmp/log.shm', 1_000_000);
        $log = Data::Log::Shared->new_memfd("my_log", 1_000_000);
        $log = Data::Log::Shared->new_from_fd($fd);

DESCRIPTION
    Append-only log in shared memory. Multiple writers append
    variable-length entries via CAS on a tail offset. Readers replay from
    any position. Entries persist until explicit "reset".

    Unlike Data::Queue::Shared (consumed on read) and Data::PubSub::Shared
    (ring overwrites), the log retains all entries until truncation. Useful
    for audit trails, event sourcing, debug logging.

    Linux-only. Requires 64-bit Perl.

METHODS
  Append
        my $off = $log->append($data);  # returns offset, or undef if full

    $data must be non-empty (empty strings are rejected since len=0 is the
    internal uncommitted marker).

  Read
        my ($data, $next_off) = $log->read_entry($offset);
        my ($data, $next_off) = $log->read_entry($offset, $abandon_wait_us);
        # returns () if no entry at offset (end of log or in-flight writer)
        # returns (undef, $next_off) if slot was abandoned by a crashed writer

    "read_entry" distinguishes three cases:

    *   Entry available: returns "($data, $next_off)" where $data is
        defined.

    *   End / in-flight: returns the empty list. The caller should stop
        iterating (or wait via "wait_for") and retry.

    *   Abandoned slot: a writer reserved the slot via CAS but died before
        committing "len". After a bounded wait (default 2s, configurable via
        the optional second argument $abandon_wait_us) the reader skips the
        slot, returning "(undef, $next_off)". Pass 0 to skip immediately
        without waiting.

    "each_entry" handles all three cases internally, silently skipping
    abandoned slots:

        my $final_pos = $log->each_entry(sub {
            my ($data, $offset) = @_;
        });
        my $final_pos = $log->each_entry(\&cb, $start_offset);
        my $final_pos = $log->each_entry(\&cb, $start_offset, $abandon_wait_us);

    $abandon_wait_us is forwarded to "read_entry"; pass 0 to skip
    uncommitted slots immediately rather than waiting up to 2s for an
    in-flight writer.

  Status
        my $off  = $log->tail_offset;   # byte offset past last entry
        my $n    = $log->entry_count;   # number of committed entries
        my $sz   = $log->data_size;     # total data region size
        my $free = $log->available;     # remaining bytes

  Waiting
        my $ok = $log->wait_for($expected_count);           # block until count changes
        my $ok = $log->wait_for($expected_count, $timeout);  # with timeout
        my $ok = $log->wait_for($expected_count, 0);         # non-blocking poll

    Returns 1 if new entries arrived (count != expected), 0 on timeout.

  Lifecycle
        $log->reset;                 # clear all (NOT concurrency-safe)
        $log->truncate($offset);     # mark entries before offset as invalid (concurrency-safe)
        my $off = $log->truncation;  # current truncation offset
        $log->sync;                  # msync to disk
        $log->unlink;                # remove backing file

  Truncation vs Reset
    The log has a fixed size ("data_size", set at creation). It is
    append-only -- space is never reclaimed automatically.

    truncate($offset) is concurrency-safe (lock-free CAS). It marks all
    entries before $offset as logically invalid -- readers calling
    "read_entry" or "each_entry" will skip them. However, truncation does
    not free physical space: the tail offset keeps advancing, and the log
    will eventually fill regardless of truncation.

    "reset" reclaims all space by zeroing the tail and the data region. It
    is not concurrency-safe -- it must only be called when no other process
    is reading or writing. Cost is O(data_size) because the slot region is
    zeroed to prevent stale-data hazards for subsequent readers.

    Typical pattern for long-running logs: size the log generously, truncate
    periodically to discard old entries from readers, and reset during
    controlled maintenance windows when the log fills.

    See "eg/truncate.pl" in the distribution for a working example.

  Common
        my $p  = $log->path;
        my $fd = $log->memfd;
        my $s  = $log->stats;

  eventfd
        my $fd = $log->eventfd;
        $log->eventfd_set($fd);
        my $fd = $log->fileno;
        $log->notify;
        my $n  = $log->eventfd_consume;

STATS
    stats() returns: "data_size", "tail", "count", "available", "waiters",
    "appends", "waits", "timeouts", "mmap_size".

SECURITY
    Backing files are created with mode 0600 (owner-only) by default, so
    only the creating user can open and attach them. To share a backing file
    across users, pass an explicit octal file mode such as 0660 as the last
    argument to "new"; the mode is applied only when the file is created (an
    existing file keeps its own permissions). The file is opened with
    "O_NOFOLLOW", so a symlink planted at the path is refused, and created
    with "O_EXCL"; the on-disk header is validated when the file is
    attached. Any process you grant write access to a shared mapping is
    trusted not to corrupt its contents while other processes are using it.

BENCHMARKS
    Single-process (1M ops, x86_64 Linux, Perl 5.40):

        append (12B entries)     8.9M/s
        append (200B entries)    8.0M/s
        read_entry sequential   4.1M/s

    Multi-process (8 workers, 200K appends each):

        concurrent append       6.2M/s aggregate

SEE ALSO
    Data::Queue::Shared - FIFO queue (consumed on read)

    Data::ReqRep::Shared - request-reply

    Data::PubSub::Shared - publish-subscribe ring (overwrites)

    Data::Stack::Shared - LIFO stack

    Data::Deque::Shared - double-ended queue

    Data::Pool::Shared - fixed-size object pool

    Data::Buffer::Shared - typed shared array

    Data::Sync::Shared - synchronization primitives

    Data::HashMap::Shared - concurrent hash table

    Data::Heap::Shared - priority queue

    Data::Graph::Shared - directed weighted graph

    Data::BitSet::Shared - shared bitset (lock-free per-bit ops)

    Data::RingBuffer::Shared - fixed-size overwriting ring buffer

AUTHOR
    vividsnow

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

