NAME
Shared::Arena::Ring - a record queue several processes can write at once
VERSION
Version 0.03
SYNOPSIS
my $arena = Shared::Arena->create(size => 8 * 1024 * 1024);
my $ring = $arena->ring('events', slots => 4096, slot_size => 512);
# any number of processes, no lock between them
$ring->publish('order.paid', $payload);
# each reader has its own position and sees everything
my $cursor = $ring->cursor;
for my $rec ($cursor->drain) {
my ($topic, $payload, $seq) = @$rec;
}
DESCRIPTION
A ring lives in an arena and is written by as many processes as care to, without a lock and without a syscall. Readers are independent: each has its own cursor, each sees every record, and a slow one never blocks a writer.
Get one from $arena->ring($name). Every process can ask for the same name with the same arguments; the first creates it and the rest attach.
A ring has a fixed size, and forgets
The ring holds a fixed number of records. When it fills, the oldest is overwritten: a publisher never blocks and never fails because a reader is behind.
Nothing is lost quietly. A reader that has been overtaken discovers it, skips to the oldest record still present, and adds what it missed to its own count. For any cursor, at any moment,
delivered + lapped + abandoned == published
so a caller can always say exactly how much it did not see. Size the ring for the slowest reader you are willing to serve, and watch lapped.
What a record can carry
A record is a topic and a payload, both arbitrary bytes, both allowed to be empty. Neither is inspected: a topic is only a label the reader may look at.
A record larger than one slot is carried across several. One larger than half the ring is refused, and publish returns -1. It is never truncated: a truncated record arrives with the right sequence and the right topic and a body that silently is not what was sent, which a reader has no way to detect.
Ask max_record for the ceiling rather than working it out. It is the total of topic and payload together, it depends on how the ring was configured, and a caller that hard-codes it starts refusing records the day somebody changes the configuration.
Records arrive in order, and whole
Every record is given a sequence when it is published, and cursors deliver in sequence order. A single publisher's records therefore always arrive in the order it wrote them, however much of the stream around them was lost.
A record is delivered whole or not at all. There is no state in which half of one and half of another arrive together.
METHODS
publish
my $seq = $ring->publish($topic, $payload);
Returns the record's sequence, which is always greater than zero. Returns 0 when there is no usable ring, and -1 when the record was refused for size.
One value rather than a pair, and deliberately not a context-sensitive one: is($ring->publish(...), 1) would call it in list context and compare its two return values against each other.
cursor
my $cursor = $ring->cursor;
my $cursor = $ring->cursor(from_start => 1);
A new reader, starting at now: it sees what happens next and does not replay what it missed. That is what a tail wants, and a process attaching to a busy ring is almost always asking about the future.
from_start => 1 begins at the oldest record the ring still holds.
Every cursor is independent. Two of them see the same records; neither consumes anything from the other.
group
my $g = $ring->group('workers');
my $g = $ring->group('workers', topic => 'jobs');
A Shared::Arena::Ring::Group: the same ring read as a queue rather than a broadcast. A cursor lives in the process that made it, so every reader sees every record; a group's cursor lives in the ring, so each record goes to exactly one member of the pool.
for my $rec ($g->claim(max => 8)) { ... } # nobody else got these
Use a cursor when every worker needs to know, and a group when the work needs doing once. Both can read one ring at the same time without interfering.
At most once: a member that claims a record and then dies loses it. See "At most once, which is a data-loss decision" in Shared::Arena::Ring::Group.
max_record
my $bytes = $ring->max_record;
The largest topic and payload together that this ring will carry. Ask, do not assume.
slot_bytes
my $bytes = $ring->slot_bytes;
What fits in a single slot. A record within this uses one slot; a larger one spans several. Useful for sizing a ring, and of no interest at publish time.
slots
my $n = $ring->slots;
stats
my %s = $ring->stats; # published, oversize, seq
Counted across every process, for the life of the ring. seq is the next sequence to be handed out.
CONFIGURING A RING
slots and slot_size are given once, by whoever creates it, and every later attacher must ask for the same shape or be refused.
my $ring = $arena->ring('events', slots => 4096, slot_size => 512);
slots is how many records the ring holds when they are small, and it is what decides how far behind a reader may fall. slot_size is the space one slot gives to a topic and payload plus a small header; make it comfortably larger than a typical record and let the unusual ones span.
Both cost memory whether used or not: a ring occupies slots * slot_size bytes for its life.
SEE ALSO
Shared::Arena, Shared::Arena::Ring::Cursor.
AUTHOR
LNATION, <email at lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under the Artistic License 2.0.