Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
lib/Algorithm/Classifier/IsolationForest/App/Command/streamd.pm view on Meta::CPAN
package Algorithm::Classifier::IsolationForest::App::Command::streamd;
use strict;
use warnings;
use Algorithm::Classifier::IsolationForest ();
use Algorithm::Classifier::IsolationForest::Online ();
use Algorithm::Classifier::IsolationForest::App -command;
use File::Slurp qw(read_file write_file);
use File::Path qw(make_path);
use File::Basename qw(dirname);
use File::Spec ();
use Scalar::Util qw(looks_like_number);
use IO::Socket::UNIX ();
use IO::Select ();
use POSIX qw(setsid strftime);
use Errno qw();
# The daemon is a singleton per process, so its runtime state lives in
# file-scoped lexicals rather than being threaded through every helper.
my $JSON; # JSON::MaybeXS codec (required at runtime, see execute)
my $OIF; # the online model
my %OPT; # resolved options
my $LOG_FH; # log handle (STDERR in foreground without --log)
my %CONN; # fileno => { sock, inbuf, outbuf, mode }
my $DIRTY = 0; # learned anything since the last save?
my $RUN; # cleared by SIGTERM/SIGINT
my $SAVE_NOW; # set by SIGUSR1
my $REOPEN_LOG; # set by SIGHUP
# Sanity caps on per-connection buffers: a client is allowed big batch
# messages, but one that streams an endless line (or stops reading its
# replies) gets dropped instead of eating the daemon's memory.
use constant MAX_INBUF => 16 * 1024 * 1024;
use constant MAX_OUTBUF => 16 * 1024 * 1024;
sub opt_spec {
return (
[
'set=s',
'Named instance. Appended to --model-dir, and the socket/pid become <set>.sock / <set>.pid '
. 'under the run dir, so several daemons run side by side with no other flags. '
. 'Must match /\A[A-Za-z0-9+\-@_]+\z/.'
],
[
'socket=s',
'Unix domain socket to listen on; default /var/run/iforest_streamd/streamd.sock. With --set '
. 'this is instead the base run dir (default /var/run/iforest_streamd) the <set>.sock is created in.',
{ 'completion' => 'files' }
],
[
'pid=s',
'Where to write the daemon pid; default /var/run/iforest_streamd/streamd.pid. With --set '
. 'this is instead the base run dir (default /var/run/iforest_streamd) the <set>.pid is created in.',
{ 'completion' => 'files' }
],
[
'model-dir=s',
'Directory timestamped model saves land in; the symlink latest.json in it always points '
. 'at the newest, and the daemon resumes from it at startup when it exists. With --set '
. 'the set name is appended as a subdirectory.',
{ 'default' => '/var/db/iforest_streamd', 'completion' => 'files' }
],
[
'save-interval=i',
'Seconds between periodic model saves (only when learning happened).',
{ 'default' => 300 }
],
[ 'keep=i', 'Prune all but the newest N timestamped model files after each save.' ],
[ 'f|foreground', 'Do not daemonize; log to stderr unless --log is given.' ],
[
'log=s',
'Log file. Defaults to <model-dir>/streamd.log when daemonized; stderr in the foreground.',
lib/Algorithm/Classifier/IsolationForest/App/Command/streamd.pm view on Meta::CPAN
if ( $cmd eq 'save' ) {
my $name = eval { _save_model('command') };
if ($@) {
( my $err = $@ ) =~ s/ at \S+ line \d+\.?\s*\z//s;
return _reply( $c, { error => $err, @$tag } );
}
return _reply( $c, { ok => { saved => $name }, @$tag } );
}
if ( $cmd eq 'relearn-threshold' ) {
my $ok = eval { $OIF->relearn_threshold; 1 };
if ( !$ok ) {
( my $err = $@ ) =~ s/ at \S+ line \d+\.?\s*\z//s;
chomp $err;
return _reply( $c, { error => $err, @$tag } );
}
return _reply( $c, { ok => { threshold => 0 + $OIF->decision_threshold }, @$tag } );
}
return _reply( $c, { error => 'unknown cmd "' . $cmd . '"', @$tag } );
} ## end sub _handle_cmd
sub _reply {
my ( $c, $reply ) = @_;
$c->{outbuf} .= $JSON->encode($reply) . "\n";
return 1;
}
# The effective label cutoff, resolved per message so relearns and the
# contamination refresh at save time take effect immediately.
sub _threshold {
return
defined $OPT{'threshold'} ? $OPT{'threshold'}
: defined $OIF->decision_threshold ? $OIF->decision_threshold
: 0.5;
}
# One row through the model. A JSON object is a tagged row (full munger
# plan, expanding/combining mungers included); a JSON array is positional
# (scalar mungers, like stream CSV input). Either way the final vector
# is validated numeric before it touches the model -- JSON delivers
# typed values, so anything non-numeric left after munging is a caller
# bug worth an explicit error rather than Perl's silent string-to-0.
# Returns the score, or undef in learn mode. Croaks on any problem.
sub _apply_row {
my ( $row, $mode ) = @_;
my $vec;
if ( ref $row eq 'HASH' ) {
$vec = $OIF->tagged_row_to_array( $row, 'streamd' );
} elsif ( ref $row eq 'ARRAY' ) {
$vec = $row;
if ( ref $OIF->{mungers} eq 'HASH' && %{ $OIF->{mungers} } ) {
$vec = $OIF->munge_rows( [$row] )->[0];
}
} else {
die 'row must be a JSON array (positional) or object (tagged)' . "\n";
}
for my $col ( 0 .. $#$vec ) {
next if !defined $vec->[$col]; # undef defers to the model's missing policy
die 'column ' . ( $col + 1 ) . ' is not a number after munging' . "\n"
unless looks_like_number( $vec->[$col] );
}
if ( $mode eq 'learn' ) {
$OIF->learn( [$vec] );
$DIRTY = 1;
return undef;
}
if ( $mode eq 'score' ) {
return $OIF->score_samples( [$vec] )->[0];
}
my $score = $OIF->score_learn( [$vec] )->[0];
$DIRTY = 1;
return $score;
} ## end sub _apply_row
return 1;
( run in 0.626 second using v1.01-cache-2.11-cpan-995e09ba956 )