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_keyslive 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:
RT: CPAN's request tracker (report bugs here)
https://rt.cpan.org/NoAuth/Bugs.html?Dist=Algorithm-EventsPerSecond
CPAN Ratings
Search CPAN
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