Algorithm-EventsPerSecond

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

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

README.md  view on Meta::CPAN


    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

t/sukkal.t  view on Meta::CPAN

#!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;



( run in 1.682 second using v1.01-cache-2.11-cpan-64ef6c95b5d )