NAME

Algorithm::EventsPerSecond::Sukkal - A unix-socket daemon serving per-key sliding-window event rates.

VERSION

Version 0.1.0

SYNOPSIS

use Algorithm::EventsPerSecond::Sukkal;

my $sukkal = Algorithm::EventsPerSecond::Sukkal->new(
    socket => '/var/run/iqbi-damiq.sock',
    window => 60,
);

$SIG{TERM} = $SIG{INT} = sub { $sukkal->stop };

$sukkal->run;    # blocks until stop()

Then, from any client:

use IO::Socket::UNIX;

my $sock = IO::Socket::UNIX->new(
    Type => SOCK_STREAM,
    Peer => '/var/run/iqbi-damiq.sock',
);

print $sock "MARK requests\n";      # fire and forget
print $sock "MARK errors 3\n";

print $sock "RATE requests\n";
my $reply = <$sock>;                # "OK 41.2\n"

print $sock "MARKRATE requests\n";  # mark and rate in one call
my $rate = <$sock>;                 # "OK 41.3\n"

DESCRIPTION

A sukkal is the vizier-messenger of a Mesopotamian court: petitioners speak to it, and it relays word of them to the throne. This sukkal listens on a unix stream socket, records events marked against arbitrary client-chosen keys, and answers queries about their rates. Each key gets its own Algorithm::EventsPerSecond meter, so mark stays O(1) and memory per key is constant regardless of event volume.

The daemon is a single process driven by a non-blocking select loop; no non-core modules are required. Marks arriving back-to-back on a connection are coalesced per key and applied with a single mark($n) call, so the hot path is dominated by socket reads and line parsing, not by the meters.

Keys that go idle longer than "idle_timeout" are evicted by a periodic sweep. Because the timeout is never shorter than the window, an evicted key by definition has zero events inside the window, so queries for it correctly read as zero; the only state lost is its lifetime "TOTAL".

The bundled launcher script is iqbi-damiq, "She said 'it is fine!'".

METHODS

new( socket => $path, %options )

Construct a daemon. Nothing is bound until "run" is called.

socket

Path of the unix socket to listen on. Required. A stale socket file left by a dead daemon is removed automatically; a live listener on the same path is an error.

window

Averaging window in seconds for every meter, as in "new" in Algorithm::EventsPerSecond. Defaults to 60. Each key's memory scales linearly with the window; see "MEMORY USAGE".

max_keys

Maximum number of distinct keys tracked at once. Marks for new keys beyond the limit are rejected with an error reply. 0 means unlimited. Defaults to 100000. This is the daemon's memory ceiling: worst case is max_keys live meters, each of a size fixed by the window; see "MEMORY USAGE".

max_key_length

Maximum key length in bytes. Keys may be any non-whitespace, non-control bytes. Defaults to 255.

idle_timeout

Seconds a key may go unmarked before the sweep evicts it. Must be at least window. Defaults to twice the window.

sweep_interval

Seconds between eviction sweeps. Defaults to 30.

max_clients

Maximum simultaneous client connections; further connections are closed immediately. 0 means unlimited, the default.

listen_backlog

The listen(2) backlog. Defaults to 128.

socket_mode

Octal permission string, e.g. '0770', applied to the socket file after binding. By default the process umask decides.

run

Bind the socket and serve until "stop" is called (typically from a signal handler; signals interrupt the select and are honored promptly). On return the socket file has been unlinked and all client connections closed. Dies if the socket cannot be bound.

stop

Ask a running daemon to shut down. Safe to call from a signal handler; the "run" loop notices on its next wakeup. Returns the daemon object.

PROTOCOL

The protocol is line-based over a unix stream socket. Lines end in \n (a trailing \r is tolerated) and hold whitespace-separated tokens; commands are case-insensitive. Keys are any non-whitespace, non-control bytes up to "max_key_length" long. Replies are a single OK ... or ERR ... line, except "KEYS" and "DUMP", which are multi-line. Commands may be pipelined freely; replies come back in order.

MARK <key> [<count>]

Record one event, or count events, against key, creating the key if it is new. Nothing is replied on success so writers never have to read; malformed input or hitting "max_keys" replies ERR ....

RATE <key>

Reply OK n with the key's events per second averaged over the window. Unknown keys read as OK 0.

MARKRATE <key> [<count>]

Record one event, or count events, against key exactly as "MARK" would, then reply OK n with the key's rate as "RATE" would — a mark and a query in a single round trip. Rejects with ERR ... under the same conditions as "MARK".

COUNT <key>

Reply OK n with the number of events inside the window. Unknown keys read as OK 0.

TOTAL <key>

Reply OK n with the key's lifetime event count. Unknown (or evicted) keys read as OK 0.

STATS [<key>]

With a key, reply OK rate=n count=n total=n window=n for it. With no key, reply the daemon's own statistics: tracked keys, connected clients, the daemon-wide mark rate and totals, uptime, window, and which Algorithm::EventsPerSecond backend is loaded.

KEYS

Reply OK n, then one key per line, then END.

DUMP

Reply OK n, then <key> <rate> <count> <total> per line, then END. Note each row costs an O(window) scan, so on huge key counts with long windows prefer targeted queries.

RESET <key>

Zero the key's meter and lifetime total, as "reset" in Algorithm::EventsPerSecond. Replies OK.

DEL <key>

Forget the key entirely. Replies OK.

PING

Replies OK PONG.

QUIT

Replies OK BYE and closes the connection.

PERFORMANCE NOTES

Batch marks: many MARK lines per write, ideally repeated keys back-to-back, or a single MARK key 1000. The daemon coalesces consecutive marks per key into one meter call, so the ceiling is socket throughput and line parsing rather than the meters — which, on the XS backend, barely notice.

Memory is bounded by "max_keys" and "window"; see "MEMORY USAGE" for how to size them.

MEMORY USAGE

Every key owns one Algorithm::EventsPerSecond meter, and a meter's size is set entirely by the window: two ring buffers of one slot per window second, counts in one and timestamps in the other. Event volume does not matter; a key marked once costs the same as a key marked a million times. The worst-case daemon footprint is therefore

max_keys * bytes_per_key

where bytes_per_key is the two buffers plus a fixed per-key overhead (the meter object, the key string, and its slot in the key table). On the XS backend a slot is a packed int64_t, so

bytes_per_key ~= 2 * 8 * window + 800

and on the pure-Perl backend a slot is a perl scalar of roughly 24 bytes, so

bytes_per_key ~= 2 * 24 * window + 2000

Measured resident-set growth per key (perl 5.42, 64-bit), and the worst case that implies at the default max_keys of 100000:

window  backend  per key   at 100000 keys
    60  XS       ~1.7 KB   ~170 MB
    60  PP       ~4.9 KB   ~490 MB
   300  XS       ~5.2 KB   ~520 MB
   300  PP       ~16 KB    ~1.6 GB

Keys are client-chosen, which is why "max_keys" exists: size it so that max_keys * bytes_per_key is something the host can absorb. The worst case only materializes if that many distinct keys are all marked within one "idle_timeout"; the sweep evicts idle keys and returns their memory.

AUTHOR

Zane C. Bowers-Hadley, <vvelox at vvelox.net>

BUGS

Please report any bugs or feature requests to bug-algorithm-eventspersecond at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Algorithm-EventsPerSecond. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc Algorithm::EventsPerSecond::Sukkal

You can also look for information at:

ACKNOWLEDGEMENTS

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by Zane C. Bowers-Hadley.

This is free software, licensed under:

The GNU Lesser General Public License, Version 2.1, February 1999