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.