File Coverage

blib/lib/Algorithm/EventsPerSecond/Sukkal.pm
Criterion Covered Total %
statement 158 271 58.3
branch 54 146 36.9
condition 25 89 28.0
subroutine 20 23 86.9
pod 3 3 100.0
total 260 532 48.8


line stmt bran cond sub pod time code
1             package Algorithm::EventsPerSecond::Sukkal;
2              
3 10     10   343326 use 5.006;
  10         28  
4 10     10   56 use strict;
  10         13  
  10         169  
5 10     10   36 use warnings;
  10         11  
  10         456  
6              
7 10     10   2666 use Errno qw(EAGAIN EWOULDBLOCK EINTR);
  10         8456  
  10         910  
8 10     10   3825 use IO::Select;
  10         12752  
  10         469  
9 10     10   4436 use IO::Socket::UNIX;
  10         116158  
  10         47  
10 10     10   3634 use Socket qw(SOCK_STREAM);
  10         15  
  10         1340  
11 10     10   1242 use Algorithm::EventsPerSecond;
  10         18  
  10         520  
12              
13             =encoding utf8
14              
15             =head1 NAME
16              
17             Algorithm::EventsPerSecond::Sukkal - A unix-socket daemon serving per-key sliding-window event rates.
18              
19             =head1 VERSION
20              
21             Version 0.1.0
22              
23             =cut
24              
25             our $VERSION = '0.1.0';
26              
27             # per-connection buffer ceilings: a single line may not span more than
28             # _RBUF_MAX, and a client that stops reading is dropped once _WBUF_MAX
29             # of replies have queued up
30             use constant {
31 10         28102 _RBUF_MAX => 1024 * 1024,
32             _WBUF_MAX => 8 * 1024 * 1024,
33             _READ_CHUNK => 65536,
34 10     10   49 };
  10         12  
35              
36             =head1 SYNOPSIS
37              
38             use Algorithm::EventsPerSecond::Sukkal;
39              
40             my $sukkal = Algorithm::EventsPerSecond::Sukkal->new(
41             socket => '/var/run/iqbi-damiq.sock',
42             window => 60,
43             );
44              
45             $SIG{TERM} = $SIG{INT} = sub { $sukkal->stop };
46              
47             $sukkal->run; # blocks until stop()
48              
49             Then, from any client:
50              
51             use IO::Socket::UNIX;
52              
53             my $sock = IO::Socket::UNIX->new(
54             Type => SOCK_STREAM,
55             Peer => '/var/run/iqbi-damiq.sock',
56             );
57              
58             print $sock "MARK requests\n"; # fire and forget
59             print $sock "MARK errors 3\n";
60              
61             print $sock "RATE requests\n";
62             my $reply = <$sock>; # "OK 41.2\n"
63              
64             print $sock "MARKRATE requests\n"; # mark and rate in one call
65             my $rate = <$sock>; # "OK 41.3\n"
66              
67             =head1 DESCRIPTION
68              
69             A sukkal is the vizier-messenger of a Mesopotamian court: petitioners
70             speak to it, and it relays word of them to the throne. This sukkal
71             listens on a unix stream socket, records events marked against
72             arbitrary client-chosen keys, and answers queries about their rates.
73             Each key gets its own L meter, so C
74             stays O(1) and memory per key is constant regardless of event volume.
75              
76             The daemon is a single process driven by a non-blocking select loop;
77             no non-core modules are required. Marks arriving back-to-back on a
78             connection are coalesced per key and applied with a single C
79             call, so the hot path is dominated by socket reads and line parsing,
80             not by the meters.
81              
82             Keys that go idle longer than L are evicted by a
83             periodic sweep. Because the timeout is never shorter than the window,
84             an evicted key by definition has zero events inside the window, so
85             queries for it correctly read as zero; the only state lost is its
86             lifetime L.
87              
88             The bundled launcher script is L, "She said 'it is fine!'".
89              
90             =head1 METHODS
91              
92             =head2 new( socket => $path, %options )
93              
94             Construct a daemon. Nothing is bound until L is called.
95              
96             =over 4
97              
98             =item socket
99              
100             Path of the unix socket to listen on. Required. A stale socket file
101             left by a dead daemon is removed automatically; a live listener on the
102             same path is an error.
103              
104             =item window
105              
106             Averaging window in seconds for every meter, as in
107             L. Defaults to 60. Each key's memory
108             scales linearly with the window; see L.
109              
110             =item max_keys
111              
112             Maximum number of distinct keys tracked at once. Marks for new keys
113             beyond the limit are rejected with an error reply. 0 means unlimited.
114             Defaults to 100000. This is the daemon's memory ceiling: worst case
115             is C live meters, each of a size fixed by the window; see
116             L.
117              
118             =item max_key_length
119              
120             Maximum key length in bytes. Keys may be any non-whitespace,
121             non-control bytes. Defaults to 255.
122              
123             =item idle_timeout
124              
125             Seconds a key may go unmarked before the sweep evicts it. Must be at
126             least C. Defaults to twice the window.
127              
128             =item sweep_interval
129              
130             Seconds between eviction sweeps. Defaults to 30.
131              
132             =item max_clients
133              
134             Maximum simultaneous client connections; further connections are
135             closed immediately. 0 means unlimited, the default.
136              
137             =item listen_backlog
138              
139             The listen(2) backlog. Defaults to 128.
140              
141             =item socket_mode
142              
143             Octal permission string, e.g. C<'0770'>, applied to the socket file
144             after binding. By default the process umask decides.
145              
146             =back
147              
148             =cut
149              
150             sub new {
151 17     17 1 251874 my ( $class, %args ) = @_;
152              
153             my $self = {
154             socket => $args{socket},
155             window => $args{window} // 60,
156             max_keys => $args{max_keys} // 100_000,
157             max_key_length => $args{max_key_length} // 255,
158             sweep_interval => $args{sweep_interval} // 30,
159             max_clients => $args{max_clients} // 0,
160 17   100     276 listen_backlog => $args{listen_backlog} // 128,
      100        
      50        
      100        
      100        
      100        
161             };
162              
163             die "socket path required\n"
164 17 100 100     92 unless defined $self->{socket} && length $self->{socket};
165              
166 15         33 for my $opt (qw(window max_key_length sweep_interval listen_backlog)) {
167             die "$opt must be a positive integer\n"
168 53 100 100     277 unless $self->{$opt} =~ /^\d+$/ && $self->{$opt} > 0;
169             }
170 11         21 for my $opt (qw(max_keys max_clients)) {
171             die "$opt must be a non-negative integer\n"
172 21 100       92 unless $self->{$opt} =~ /^\d+$/;
173             }
174              
175 9   66     34 $self->{idle_timeout} = $args{idle_timeout} // $self->{window} * 2;
176             die "idle_timeout must be an integer >= window\n"
177             unless $self->{idle_timeout} =~ /^\d+$/
178 9 100 66     52 && $self->{idle_timeout} >= $self->{window};
179              
180 8 100       16 if ( defined $args{socket_mode} ) {
181             die "socket_mode must be an octal string, e.g. '0770'\n"
182 3 100       30 unless $args{socket_mode} =~ /^0?[0-7]{3}$/;
183 1         3 $self->{socket_mode} = oct $args{socket_mode};
184             }
185              
186 6         19 $self->{meters} = {}; # key => { m => meter, seen => epoch }
187 6         12 $self->{conns} = {}; # fd => { fh, id, rbuf, wbuf, closing }
188 6         13 $self->{running} = 0;
189 6         14 $self->{started} = time();
190 6         43 $self->{self_meter} = Algorithm::EventsPerSecond->new( window => $self->{window} );
191 6         87 $self->{key_re} = qr/^[\x21-\x7E\x80-\xFF]{1,$self->{max_key_length}}$/;
192              
193 6         29 return bless $self, $class;
194             } ## end sub new
195              
196             =head2 run
197              
198             Bind the socket and serve until L is called (typically from a
199             signal handler; signals interrupt the select and are honored
200             promptly). On return the socket file has been unlinked and all client
201             connections closed. Dies if the socket cannot be bound.
202              
203             =cut
204              
205             sub run {
206 3     3 1 35 my ($self) = @_;
207              
208 3 50       15 die "already running\n" if $self->{running};
209              
210 3         25 $self->_listen;
211 1         12 local $SIG{PIPE} = 'IGNORE';
212              
213 1         6 my $rsel = $self->{rsel} = IO::Select->new( $self->{listener} );
214 1         57 my $wsel = $self->{wsel} = IO::Select->new;
215              
216 1         7 $self->{running} = 1;
217 1         2 $self->{next_sweep} = time() + $self->{sweep_interval};
218              
219 1         4 while ( $self->{running} ) {
220 3         7 my $timeout = $self->{next_sweep} - time();
221 3 50       8 $timeout = 0 if $timeout < 0;
222              
223 3         17 my ( $r, $w ) = IO::Select->select( $rsel, $wsel, undef, $timeout );
224              
225 3 100       19116 for my $fh ( @{ $r || [] } ) {
  3         35  
226 2 100       12 if ( fileno($fh) == $self->{listener_fd} ) {
227 1         8 $self->_accept;
228             } else {
229 1         6 $self->_read_client($fh);
230             }
231             }
232              
233 3 100       8 for my $fh ( @{ $w || [] } ) {
  3         12  
234             # a connection dropped during the read pass may still be in
235             # this list; its handle is closed, so fileno is undef
236 0         0 my $id = fileno $fh;
237 0 0 0     0 next unless defined $id && $self->{conns}{$id};
238 0         0 $self->_flush( $self->{conns}{$id} );
239             }
240              
241 3 50       16 if ( time() >= $self->{next_sweep} ) {
242 0         0 $self->_sweep;
243 0         0 $self->{next_sweep} = time() + $self->{sweep_interval};
244             }
245             } ## end while ( $self->{running} )
246              
247 1         4 $self->_shutdown;
248 1         39 return $self;
249             } ## end sub run
250              
251             =head2 stop
252              
253             Ask a running daemon to shut down. Safe to call from a signal handler;
254             the L loop notices on its next wakeup. Returns the daemon
255             object.
256              
257             =cut
258              
259             sub stop {
260 1     1 1 3 my ($self) = @_;
261 1         3 $self->{running} = 0;
262 1         7 return $self;
263             }
264              
265             sub _listen {
266 3     3   6 my ($self) = @_;
267 3         8 my $path = $self->{socket};
268              
269 3 100       54 if ( -e $path ) {
270 2 100       14 die "$path exists and is not a socket\n" unless -S _;
271 1         17 my $probe = IO::Socket::UNIX->new( Type => SOCK_STREAM, Peer => $path );
272 1 50       263 die "something is already listening on $path\n" if $probe;
273 0 0       0 unlink $path or die "cannot remove stale socket $path: $!\n";
274             }
275              
276             my $listener = IO::Socket::UNIX->new(
277             Type => SOCK_STREAM,
278             Local => $path,
279             Listen => $self->{listen_backlog},
280 1 50       10 ) or die "cannot listen on $path: $!\n";
281 1         316 $listener->blocking(0);
282              
283 1 50       16 chmod $self->{socket_mode}, $path if defined $self->{socket_mode};
284              
285 1         2 $self->{listener} = $listener;
286 1         2 $self->{listener_fd} = fileno $listener;
287 1         2 return;
288             } ## end sub _listen
289              
290             sub _accept {
291 1     1   3 my ($self) = @_;
292              
293 1         38 while ( my $fh = $self->{listener}->accept ) {
294 1 50 33     275 if ( $self->{max_clients}
295 0         0 && keys %{ $self->{conns} } >= $self->{max_clients} )
296             {
297 0         0 close $fh;
298 0         0 next;
299             }
300 1         7 $fh->blocking(0);
301 1         61 my $id = fileno $fh;
302 1         15 $self->{conns}{$id} = {
303             fh => $fh,
304             id => $id,
305             rbuf => '',
306             wbuf => '',
307             closing => 0,
308             };
309 1         8 $self->{rsel}->add($fh);
310             } ## end while ( my $fh = $self->{listener}->accept )
311 1         276 return;
312             } ## end sub _accept
313              
314             sub _read_client {
315 1     1   2 my ( $self, $fh ) = @_;
316 1         2 my $id = fileno $fh;
317 1 50       5 my $c = $self->{conns}{$id} or return;
318              
319 1         2 my $eof;
320             # bounded so one firehose client cannot starve the rest of the loop
321 1         3 for ( 1 .. 16 ) {
322 1         43 my $n = sysread $fh, my $chunk, _READ_CHUNK;
323 1 50       50 if ( !defined $n ) {
324 0 0 0     0 last if $!{EAGAIN} || $!{EWOULDBLOCK} || $!{EINTR};
      0        
325 0         0 $eof = 1;
326 0         0 last;
327             }
328 1 50       9 if ( $n == 0 ) { $eof = 1; last }
  0         0  
  0         0  
329 1         9 $c->{rbuf} .= $chunk;
330 1 50       4 last if $n < _READ_CHUNK;
331             } ## end for ( 1 .. 16 )
332              
333 1 50       3 if ( length $c->{rbuf} > _RBUF_MAX ) {
334 0         0 $self->_send( $c, "ERR line too long\n" );
335 0         0 return $self->_drop($c);
336             }
337              
338 1         4 $self->_process($c);
339 1 0 33     4 $self->_drop($c) if $eof && $self->{conns}{$id};
340 1         4 return;
341             } ## end sub _read_client
342              
343             sub _process {
344 1     1   2 my ( $self, $c ) = @_;
345              
346 1         3 my $buf = $c->{rbuf};
347 1         2 my $pos = 0;
348 1         1 my %pending;
349 1         2 my $pending_new = 0;
350              
351 1         3 while ( ( my $nl = index $buf, "\n", $pos ) >= 0 ) {
352 1         3 my $line = substr $buf, $pos, $nl - $pos;
353 1         1 $pos = $nl + 1;
354 1         3 $line =~ s/\r\z//;
355              
356 1         4 my ( $cmd, $key, $extra ) = split ' ', $line, 3;
357 1 50       2 next unless defined $cmd;
358 1         3 $cmd = uc $cmd;
359 1 50       2 $extra =~ s/\s+\z// if defined $extra;
360              
361             # hot path: marks are coalesced per key and applied in one
362             # mark($n) call, either just before the next query or at the
363             # end of the buffer
364 1 50       13 if ( $cmd eq 'MARK' ) {
365 0         0 my $count = 1;
366 0 0       0 if ( defined $extra ) {
367 0 0 0     0 if ( $extra =~ /^\d{1,15}$/ && $extra > 0 ) {
368 0         0 $count = $extra;
369             } else {
370 0         0 $self->_send( $c, "ERR bad count\n" );
371 0         0 next;
372             }
373             }
374 0 0 0     0 if ( !defined $key || $key !~ $self->{key_re} ) {
375 0         0 $self->_send( $c, "ERR bad key\n" );
376 0         0 next;
377             }
378 0 0 0     0 if ( !exists $self->{meters}{$key} && !exists $pending{$key} ) {
379 0 0 0     0 if ( $self->{max_keys}
380 0         0 && keys( %{ $self->{meters} } ) + $pending_new >= $self->{max_keys} )
381             {
382 0         0 $self->_send( $c, "ERR key limit reached\n" );
383 0         0 next;
384             }
385 0         0 $pending_new++;
386             }
387 0         0 $pending{$key} += $count;
388 0         0 next;
389             } ## end if ( $cmd eq 'MARK' )
390              
391 1 50       3 if (%pending) {
392 0         0 $self->_apply_marks( \%pending );
393 0         0 %pending = ();
394 0         0 $pending_new = 0;
395             }
396 1         11 $self->_command( $c, $cmd, $key, $extra );
397 1 50       16 last if $c->{closing};
398             } ## end while ( ( my $nl = index $buf, "\n", $pos ) >=...)
399              
400 1 50       3 $self->_apply_marks( \%pending ) if %pending;
401 1 50       5 $c->{rbuf} = $pos ? substr( $buf, $pos ) : $buf;
402 1         3 return;
403             } ## end sub _process
404              
405             sub _apply_marks {
406 0     0   0 my ( $self, $pending ) = @_;
407 0         0 my $meters = $self->{meters};
408 0         0 my $now = time();
409 0         0 my $n = 0;
410              
411 0         0 for my $key ( keys %$pending ) {
412 0   0     0 my $e = $meters->{$key} ||= { m => Algorithm::EventsPerSecond->new( window => $self->{window} ) };
413 0         0 $e->{m}->mark( $pending->{$key} );
414 0         0 $e->{seen} = $now;
415 0         0 $n += $pending->{$key};
416             }
417 0         0 $self->{self_meter}->mark($n);
418 0         0 return;
419             } ## end sub _apply_marks
420              
421             sub _command {
422 1     1   2 my ( $self, $c, $cmd, $key, $extra ) = @_;
423 1         2 my $meters = $self->{meters};
424              
425 1 50 33     9 if ( $cmd eq 'RATE' || $cmd eq 'COUNT' || $cmd eq 'TOTAL' ) {
      33        
426             return $self->_send( $c, "ERR bad key\n" )
427 0 0 0     0 unless defined $key && $key =~ $self->{key_re};
428 0         0 my $e = $meters->{$key};
429             my $v
430             = !$e ? 0
431             : $cmd eq 'RATE' ? sprintf( '%.6g', $e->{m}->rate )
432             : $cmd eq 'COUNT' ? $e->{m}->count
433 0 0       0 : $e->{m}->total;
    0          
    0          
434 0         0 return $self->_send( $c, "OK $v\n" );
435             } ## end if ( $cmd eq 'RATE' || $cmd eq 'COUNT' || ...)
436              
437 1 50       6 if ( $cmd eq 'MARKRATE' ) {
438 0         0 my $count = 1;
439 0 0       0 if ( defined $extra ) {
440 0 0 0     0 return $self->_send( $c, "ERR bad count\n" )
441             unless $extra =~ /^\d{1,15}$/ && $extra > 0;
442 0         0 $count = $extra;
443             }
444             return $self->_send( $c, "ERR bad key\n" )
445 0 0 0     0 unless defined $key && $key =~ $self->{key_re};
446 0 0 0     0 if ( !exists $meters->{$key}
      0        
447             && $self->{max_keys}
448             && keys(%$meters) >= $self->{max_keys} )
449             {
450 0         0 return $self->_send( $c, "ERR key limit reached\n" );
451             }
452 0         0 $self->_apply_marks( { $key => $count } );
453 0         0 return $self->_send( $c, sprintf( "OK %.6g\n", $meters->{$key}{m}->rate ) );
454             } ## end if ( $cmd eq 'MARKRATE' )
455              
456 1 50       3 if ( $cmd eq 'STATS' ) {
457 0 0       0 if ( defined $key ) {
458             return $self->_send( $c, "ERR bad key\n" )
459 0 0       0 unless $key =~ $self->{key_re};
460 0         0 my $e = $meters->{$key};
461             my ( $rate, $count, $total )
462             = $e
463             ? ( sprintf( '%.6g', $e->{m}->rate ), $e->{m}->count, $e->{m}->total )
464 0 0       0 : ( 0, 0, 0 );
465 0         0 return $self->_send( $c, "OK rate=$rate count=$count total=$total window=$self->{window}\n" );
466             } ## end if ( defined $key )
467 0         0 my $sm = $self->{self_meter};
468             return $self->_send(
469             $c,
470             sprintf "OK keys=%d clients=%d rate=%.6g count=%d total=%d uptime=%d window=%d backend=%s\n",
471             scalar keys %$meters,
472 0         0 scalar keys %{ $self->{conns} },
473             $sm->rate, $sm->count, $sm->total, time() - $self->{started},
474 0         0 $self->{window}, Algorithm::EventsPerSecond->backend
475             );
476             } ## end if ( $cmd eq 'STATS' )
477              
478 1 50       3 if ( $cmd eq 'KEYS' ) {
479 0         0 my @keys = sort keys %$meters;
480 0         0 my $out = 'OK ' . scalar(@keys) . "\n";
481 0         0 $out .= "$_\n" for @keys;
482 0         0 return $self->_send( $c, $out . "END\n" );
483             }
484              
485 1 50       3 if ( $cmd eq 'DUMP' ) {
486 0         0 my @keys = sort keys %$meters;
487 0         0 my $out = 'OK ' . scalar(@keys) . "\n";
488 0         0 for my $k (@keys) {
489 0         0 my $m = $meters->{$k}{m};
490 0         0 $out .= sprintf "%s %.6g %d %d\n", $k, $m->rate, $m->count, $m->total;
491             }
492 0         0 return $self->_send( $c, $out . "END\n" );
493             }
494              
495 1 50       3 if ( $cmd eq 'RESET' ) {
496             return $self->_send( $c, "ERR bad key\n" )
497 0 0 0     0 unless defined $key && $key =~ $self->{key_re};
498 0 0       0 if ( my $e = $meters->{$key} ) {
499 0         0 $e->{m}->reset;
500 0         0 $e->{seen} = time();
501             }
502 0         0 return $self->_send( $c, "OK\n" );
503             }
504              
505 1 50       3 if ( $cmd eq 'DEL' ) {
506             return $self->_send( $c, "ERR bad key\n" )
507 0 0 0     0 unless defined $key && $key =~ $self->{key_re};
508 0         0 delete $meters->{$key};
509 0         0 return $self->_send( $c, "OK\n" );
510             }
511              
512 1 50       2 if ( $cmd eq 'PING' ) {
513 1         3 return $self->_send( $c, "OK PONG\n" );
514             }
515              
516 0 0       0 if ( $cmd eq 'QUIT' ) {
517 0         0 $c->{closing} = 1;
518 0         0 $self->{rsel}->remove( $c->{fh} );
519 0         0 return $self->_send( $c, "OK BYE\n" );
520             }
521              
522 0         0 return $self->_send( $c, "ERR unknown command\n" );
523             } ## end sub _command
524              
525             sub _send {
526 1     1   3 my ( $self, $c, $data ) = @_;
527              
528 1 50       2 if ( $c->{wbuf} eq '' ) {
529 1         26 my $n = syswrite $c->{fh}, $data;
530 1 50 0     9 if ( defined $n ) {
    0          
531 1 50       3 if ( $n == length $data ) {
532 1 50       4 $self->_drop($c) if $c->{closing};
533 1         4 return;
534             }
535 0         0 $data = substr $data, $n;
536             } elsif ( !( $!{EAGAIN} || $!{EWOULDBLOCK} || $!{EINTR} ) ) {
537 0         0 return $self->_drop($c);
538             }
539             } ## end if ( $c->{wbuf} eq '' )
540              
541 0         0 $c->{wbuf} .= $data;
542 0 0       0 return $self->_drop($c) if length $c->{wbuf} > _WBUF_MAX;
543 0         0 $self->{wsel}->add( $c->{fh} );
544 0         0 return;
545             } ## end sub _send
546              
547             sub _flush {
548 0     0   0 my ( $self, $c ) = @_;
549              
550 0         0 my $n = syswrite $c->{fh}, $c->{wbuf};
551 0 0       0 if ( !defined $n ) {
552 0 0 0     0 return if $!{EAGAIN} || $!{EWOULDBLOCK} || $!{EINTR};
      0        
553 0         0 return $self->_drop($c);
554             }
555 0         0 substr( $c->{wbuf}, 0, $n ) = '';
556 0 0       0 if ( $c->{wbuf} eq '' ) {
557 0         0 $self->{wsel}->remove( $c->{fh} );
558 0 0       0 $self->_drop($c) if $c->{closing};
559             }
560 0         0 return;
561             } ## end sub _flush
562              
563             sub _drop {
564 1     1   3 my ( $self, $c ) = @_;
565 1         2 my $fh = $c->{fh};
566 1         4 $self->{rsel}->remove($fh);
567 1         39 $self->{wsel}->remove($fh);
568 1         22 delete $self->{conns}{ $c->{id} };
569 1         18 close $fh;
570 1         7 return;
571             }
572              
573             sub _sweep {
574 0     0   0 my ($self) = @_;
575 0         0 my $meters = $self->{meters};
576 0         0 my $cutoff = time() - $self->{idle_timeout};
577 0         0 delete @$meters{ grep { $meters->{$_}{seen} < $cutoff } keys %$meters };
  0         0  
578 0         0 return;
579             }
580              
581             sub _shutdown {
582 1     1   2 my ($self) = @_;
583 1         1 $self->_drop($_) for values %{ $self->{conns} };
  1         6  
584 1 50       5 if ( $self->{listener} ) {
585 1         11 close delete $self->{listener};
586 1         70 unlink $self->{socket};
587             }
588 1         12 delete @{$self}{qw(rsel wsel listener_fd)};
  1         4  
589 1         2 return;
590             } ## end sub _shutdown
591              
592             =head1 PROTOCOL
593              
594             The protocol is line-based over a unix stream socket. Lines end in
595             C<\n> (a trailing C<\r> is tolerated) and hold whitespace-separated
596             tokens; commands are case-insensitive. Keys are any non-whitespace,
597             non-control bytes up to L long. Replies are a single
598             C or C line, except L and L, which are
599             multi-line. Commands may be pipelined freely; replies come back in
600             order.
601              
602             =head2 MARK []
603              
604             Record one event, or C events, against C, creating the key
605             if it is new. Nothing is replied on success
606             so writers never have to read; malformed input or hitting
607             L replies C.
608              
609             =head2 RATE
610              
611             Reply C with the key's events per second averaged over the
612             window. Unknown keys read as C.
613              
614             =head2 MARKRATE []
615              
616             Record one event, or C events, against C exactly as
617             L would, then reply C with the key's rate as L
618             would — a mark and a query in a single round trip. Rejects with
619             C under the same conditions as L.
620              
621             =head2 COUNT
622              
623             Reply C with the number of events inside the window. Unknown
624             keys read as C.
625              
626             =head2 TOTAL
627              
628             Reply C with the key's lifetime event count. Unknown (or
629             evicted) keys read as C.
630              
631             =head2 STATS []
632              
633             With a key, reply C for it. With
634             no key, reply the daemon's own statistics: tracked keys, connected
635             clients, the daemon-wide mark rate and totals, uptime, window, and
636             which L backend is loaded.
637              
638             =head2 KEYS
639              
640             Reply C, then one key per line, then C.
641              
642             =head2 DUMP
643              
644             Reply C, then C<< >> per line, then
645             C. Note each row costs an O(window) scan, so on huge key counts
646             with long windows prefer targeted queries.
647              
648             =head2 RESET
649              
650             Zero the key's meter and lifetime total, as
651             L. Replies C.
652              
653             =head2 DEL
654              
655             Forget the key entirely. Replies C.
656              
657             =head2 PING
658              
659             Replies C.
660              
661             =head2 QUIT
662              
663             Replies C and closes the connection.
664              
665             =head1 PERFORMANCE NOTES
666              
667             Batch marks: many C lines per write, ideally repeated keys
668             back-to-back, or a single C. The daemon coalesces
669             consecutive marks per key into one meter call, so the ceiling is
670             socket throughput and line parsing rather than the meters — which, on
671             the XS backend, barely notice.
672              
673             Memory is bounded by L and L; see L
674             USAGE> for how to size them.
675              
676             =head1 MEMORY USAGE
677              
678             Every key owns one L meter, and a meter's
679             size is set entirely by the window: two ring buffers of one slot per
680             window second, counts in one and timestamps in the other. Event
681             volume does not matter; a key marked once costs the same as a key
682             marked a million times. The worst-case daemon footprint is therefore
683              
684             max_keys * bytes_per_key
685              
686             where bytes_per_key is the two buffers plus a fixed per-key overhead
687             (the meter object, the key string, and its slot in the key table).
688             On the XS backend a slot is a packed C, so
689              
690             bytes_per_key ~= 2 * 8 * window + 800
691              
692             and on the pure-Perl backend a slot is a perl scalar of roughly 24
693             bytes, so
694              
695             bytes_per_key ~= 2 * 24 * window + 2000
696              
697             Measured resident-set growth per key (perl 5.42, 64-bit), and the
698             worst case that implies at the default max_keys of 100000:
699              
700             window backend per key at 100000 keys
701             60 XS ~1.7 KB ~170 MB
702             60 PP ~4.9 KB ~490 MB
703             300 XS ~5.2 KB ~520 MB
704             300 PP ~16 KB ~1.6 GB
705              
706             Keys are client-chosen, which is why L exists: size it so
707             that C is something the host can absorb.
708             The worst case only materializes if that many distinct keys are all
709             marked within one L; the sweep evicts idle keys and
710             returns their memory.
711              
712             =head1 AUTHOR
713              
714             Zane C. Bowers-Hadley, C<< >>
715              
716             =head1 BUGS
717              
718             Please report any bugs or feature requests to C, or through
719             the web interface at L. I will be notified, and then you'll
720             automatically be notified of progress on your bug as I make changes.
721              
722             =head1 SUPPORT
723              
724             You can find documentation for this module with the perldoc command.
725              
726             perldoc Algorithm::EventsPerSecond::Sukkal
727              
728             You can also look for information at:
729              
730             =over 4
731              
732             =item * RT: CPAN's request tracker (report bugs here)
733              
734             L
735              
736             =item * CPAN Ratings
737              
738             L
739              
740             =item * Search CPAN
741              
742             L
743              
744             =back
745              
746             =head1 ACKNOWLEDGEMENTS
747              
748              
749             =head1 LICENSE AND COPYRIGHT
750              
751             This software is Copyright (c) 2026 by Zane C. Bowers-Hadley.
752              
753             This is free software, licensed under:
754              
755             The GNU Lesser General Public License, Version 2.1, February 1999
756              
757              
758             =cut
759              
760             1; # End of Algorithm::EventsPerSecond::Sukkal