view release on metacpan or search on metacpan
Revision history for Algorithm-EventsPerSecond
0.1.0 2026-07-09/08:15
- Add the vizier Iqbi-damiq at src_bin/iqbi-damiq, a unix-socket
daemon with the backend of Algorithm::EventsPerSecond::Sukkal,
for the purpose of being able to simplify EPS tracking for multiple
items in a shared manner.
- Add rc/ with example startup scripts for iqbi-damiq:
FreeBSD rc.d and Linux systemd.
0.0.1 2026-07-04/13:30
- init
printf "current rate: %.2f events/sec\n", $meter->rate;
}
print "events seen in window: ", $meter->count, "\n";
print "lifetime total: ", $meter->total, "\n";
```
## The iqbi-damiq daemon
The dist ships `iqbi-damiq`, a unix-socket daemon built on
`Algorithm::EventsPerSecond::Sukkal`. Clients mark events against keys of
their choosing and query per-key rates over a simple line protocol; each key
gets its own meter, idle keys are evicted automatically, and marks are
coalesced so the hot path is socket I/O, not the meters.
```sh
iqbi-damiq -s /var/run/iqbi-damiq.sock -w 60
printf 'MARK requests 5\nRATE requests\nQUIT\n' \
| socat - UNIX:/var/run/iqbi-damiq.sock
lib/Algorithm/EventsPerSecond/Sukkal.pm view on Meta::CPAN
use Errno qw(EAGAIN EWOULDBLOCK EINTR);
use IO::Select;
use IO::Socket::UNIX;
use Socket qw(SOCK_STREAM);
use Algorithm::EventsPerSecond;
=encoding utf8
=head1 NAME
Algorithm::EventsPerSecond::Sukkal - A unix-socket daemon serving per-key sliding-window event rates.
=head1 VERSION
Version 0.1.0
=cut
our $VERSION = '0.1.0';
# per-connection buffer ceilings: a single line may not span more than
lib/Algorithm/EventsPerSecond/Sukkal.pm view on Meta::CPAN
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"
=head1 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 L<Algorithm::EventsPerSecond> meter, so C<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 C<mark($n)>
call, so the hot path is dominated by socket reads and line parsing,
not by the meters.
lib/Algorithm/EventsPerSecond/Sukkal.pm view on Meta::CPAN
=head1 METHODS
=head2 new( socket => $path, %options )
Construct a daemon. Nothing is bound until L</run> is called.
=over 4
=item 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.
=item window
Averaging window in seconds for every meter, as in
L<Algorithm::EventsPerSecond/new>. Defaults to 60. Each key's memory
scales linearly with the window; see L</MEMORY USAGE>.
=item max_keys
lib/Algorithm/EventsPerSecond/Sukkal.pm view on Meta::CPAN
if ( $self->{listener} ) {
close delete $self->{listener};
unlink $self->{socket};
}
delete @{$self}{qw(rsel wsel listener_fd)};
return;
} ## end sub _shutdown
=head1 PROTOCOL
The protocol is line-based over a unix stream socket. Lines end in
C<\n> (a trailing C<\r> is tolerated) and hold whitespace-separated
tokens; commands are case-insensitive. Keys are any non-whitespace,
non-control bytes up to L</max_key_length> long. Replies are a single
C<OK ...> or C<ERR ...> line, except L</KEYS> and L</DUMP>, which are
multi-line. Commands may be pipelined freely; replies come back in
order.
=head2 MARK <key> [<count>]
Record one event, or C<count> events, against C<key>, creating the key
rc/freebsd/iqbi_damiq view on Meta::CPAN
#
# PROVIDE: iqbi_damiq
# REQUIRE: DAEMON
# KEYWORD: shutdown
#
# rc.d script for iqbi-damiq, the Algorithm::EventsPerSecond::Sukkal
# daemon. Install as /usr/local/etc/rc.d/iqbi_damiq and add to
# /etc/rc.conf or /etc/rc.conf.local:
#
# iqbi_damiq_enable (bool): Set to "YES" to enable. Default "NO".
# iqbi_damiq_socket (path): Unix socket to listen on.
# Default "/var/run/iqbi-damiq/iqbi-damiq.sock".
# iqbi_damiq_socket_mode (octal): Socket file mode. Clients need write
# permission to connect; tighten to 0660
# and a shared group where any local
# user marking keys is unwanted.
# Default "0666".
# iqbi_damiq_window (int): Averaging window in seconds.
# Default "60".
# iqbi_damiq_max_keys (int): Distinct-key ceiling, 0 for
# unlimited. Default "100000".
rc/systemd/iqbi-damiq.service view on Meta::CPAN
ExecStart=/usr/local/bin/iqbi-damiq -s /run/iqbi-damiq/iqbi-damiq.sock \
-w ${IQBI_DAMIQ_WINDOW} \
--max-keys ${IQBI_DAMIQ_MAX_KEYS} \
--idle-timeout ${IQBI_DAMIQ_IDLE_TIMEOUT} \
--sweep-interval ${IQBI_DAMIQ_SWEEP_INTERVAL} \
-m ${IQBI_DAMIQ_SOCKET_MODE}
DynamicUser=yes
RuntimeDirectory=iqbi-damiq
Restart=on-failure
# Hardening: the daemon needs nothing but its unix socket.
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
PrivateNetwork=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_UNIX
src_bin/iqbi-damiq view on Meta::CPAN
$sukkal->run;
print "iqbi-damiq: shut down\n" unless $daemonize;
exit 0;
__END__
=head1 NAME
iqbi-damiq - a unix-socket daemon tracking per-key event rates
=head1 SYNOPSIS
iqbi-damiq -s <socket> [options]
iqbi-damiq -s /var/run/iqbi-damiq.sock
iqbi-damiq -s /var/run/iqbi-damiq.sock -w 300 --max-keys 500000
iqbi-damiq -s /var/run/iqbi-damiq.sock -d -p /var/run/iqbi-damiq.pid
=head1 DESCRIPTION
iqbi-damiq ("She said 'it is fine!'") runs a
L<Algorithm::EventsPerSecond::Sukkal> daemon: clients connect to the
unix socket, mark events against keys of their choosing, and query the
sliding-window rate, in-window count, and lifetime total per key. See
L<Algorithm::EventsPerSecond::Sukkal/PROTOCOL> for the line protocol.
A quick poke with socat:
printf 'MARK requests 5\nRATE requests\nQUIT\n' \
| socat - UNIX:/var/run/iqbi-damiq.sock
Or C<MARKRATE> to mark and read the rate back in a single command:
src_bin/iqbi-damiq view on Meta::CPAN
By default the daemon stays in the foreground, which suits systemd and
similar supervisors; C<-d> for classic double-fork daemonization.
=head1 OPTIONS
=over 4
=item -s, --socket PATH
Unix socket path to listen on. Required.
=item -w, --window SECONDS
Averaging window used for every meter. Default 60.
=item --max-keys N
Maximum distinct keys tracked at once; 0 for unlimited. Default
100000.
t/iqbi-damiq.t view on Meta::CPAN
# The iqbi-damiq launcher: CLI validation, and one end-to-end run
# covering the pidfile lifecycle and clean shutdown.
use 5.006;
use strict;
use warnings;
use Test::More;
BEGIN {
plan skip_all => 'unix domain sockets and fork required'
if $^O eq 'MSWin32';
}
use File::Temp qw(tempdir);
use IO::Socket::UNIX;
use Socket qw(SOCK_STREAM);
use POSIX ();
use Time::HiRes qw(sleep);
my $script = 'src_bin/iqbi-damiq';
t/sukkal-lifecycle.t view on Meta::CPAN
# Daemon lifecycle: constructor validation, socket path handling,
# socket permissions, the client cap, concurrent multiplexing of
# multiple clients, and idle-key eviction.
use 5.006;
use strict;
use warnings;
use Test::More;
BEGIN {
plan skip_all => 'unix domain sockets and fork required'
if $^O eq 'MSWin32';
}
use lib 't/lib';
use File::Temp qw(tempdir);
use Time::HiRes qw(sleep);
use Algorithm::EventsPerSecond::Sukkal;
use Sukkal_TestUtil qw(spawn_daemon connect_daemon read_line req req_multi stop_daemon);
alarm 120; # watchdog: a hung daemon must fail the test, not the harness
t/sukkal-protocol.t view on Meta::CPAN
# Protocol edge cases: input fragmentation and reassembly, line
# framing, and the validation boundaries of the wire protocol.
use 5.006;
use strict;
use warnings;
use Test::More;
BEGIN {
plan skip_all => 'unix domain sockets and fork required'
if $^O eq 'MSWin32';
}
use lib 't/lib';
use Time::HiRes qw(sleep);
use Sukkal_TestUtil qw(spawn_daemon connect_daemon read_line req req_multi stop_daemon);
$SIG{PIPE} = 'IGNORE';
alarm 120; # watchdog: a hung daemon must fail the test, not the harness
#!perl
use 5.006;
use strict;
use warnings;
use Test::More;
BEGIN {
plan skip_all => 'unix domain sockets and fork required'
if $^O eq 'MSWin32';
}
use File::Temp qw(tempdir);
use IO::Socket::UNIX;
use Socket qw(SOCK_STREAM);
use POSIX ();
use Time::HiRes qw(sleep);
use Algorithm::EventsPerSecond::Sukkal;