| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Algorithm::Classifier::IsolationForest::App::Command::streamc; |
|
2
|
|
|
|
|
|
|
|
|
3
|
81
|
|
|
81
|
|
56274
|
use strict; |
|
|
81
|
|
|
|
|
161
|
|
|
|
81
|
|
|
|
|
2654
|
|
|
4
|
81
|
|
|
81
|
|
308
|
use warnings; |
|
|
81
|
|
|
|
|
131
|
|
|
|
81
|
|
|
|
|
3573
|
|
|
5
|
81
|
|
|
81
|
|
353
|
use Algorithm::Classifier::IsolationForest::App -command; |
|
|
81
|
|
|
|
|
122
|
|
|
|
81
|
|
|
|
|
569
|
|
|
6
|
81
|
|
|
81
|
|
23648
|
use File::Slurp qw(write_file); |
|
|
81
|
|
|
|
|
175
|
|
|
|
81
|
|
|
|
|
4340
|
|
|
7
|
81
|
|
|
81
|
|
402
|
use File::Spec (); |
|
|
81
|
|
|
|
|
155
|
|
|
|
81
|
|
|
|
|
1725
|
|
|
8
|
81
|
|
|
81
|
|
303
|
use Scalar::Util qw(looks_like_number); |
|
|
81
|
|
|
|
|
137
|
|
|
|
81
|
|
|
|
|
3226
|
|
|
9
|
81
|
|
|
81
|
|
34847
|
use IO::Socket::UNIX (); |
|
|
81
|
|
|
|
|
914765
|
|
|
|
81
|
|
|
|
|
1702
|
|
|
10
|
81
|
|
|
81
|
|
32481
|
use IO::Select (); |
|
|
81
|
|
|
|
|
106779
|
|
|
|
81
|
|
|
|
|
157260
|
|
|
11
|
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
# JSON::MaybeXS codec and the connected socket, set up in execute. |
|
13
|
|
|
|
|
|
|
my $JSON; |
|
14
|
|
|
|
|
|
|
my $SOCK; |
|
15
|
|
|
|
|
|
|
my $TIMEOUT; |
|
16
|
|
|
|
|
|
|
my $READ_BUF = ''; |
|
17
|
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
sub opt_spec { |
|
19
|
|
|
|
|
|
|
return ( |
|
20
|
|
|
|
|
|
|
[ |
|
21
|
23
|
|
|
23
|
1
|
599547
|
'set=s', |
|
22
|
|
|
|
|
|
|
'Named streamd instance to talk to; the socket becomes .sock under the run dir, ' |
|
23
|
|
|
|
|
|
|
. 'exactly as streamd resolves it. Must match /\A[A-Za-z0-9+\-@_]+\z/.' |
|
24
|
|
|
|
|
|
|
], |
|
25
|
|
|
|
|
|
|
[ |
|
26
|
|
|
|
|
|
|
'socket=s', |
|
27
|
|
|
|
|
|
|
'Unix domain socket streamd listens on; default /var/run/iforest_streamd/streamd.sock. With ' |
|
28
|
|
|
|
|
|
|
. '--set this is instead the base run dir (default /var/run/iforest_streamd) holding .sock.', |
|
29
|
|
|
|
|
|
|
{ 'completion' => 'files' } |
|
30
|
|
|
|
|
|
|
], |
|
31
|
|
|
|
|
|
|
[ 'timeout=i', 'Seconds to wait for each reply from the daemon.', { 'default' => 30 } ], |
|
32
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
# stream mode |
|
34
|
|
|
|
|
|
|
[ |
|
35
|
|
|
|
|
|
|
'i=s', |
|
36
|
|
|
|
|
|
|
'Input to stream through the daemon, one row per line; - reads stdin.', |
|
37
|
|
|
|
|
|
|
{ 'completion' => 'files' } |
|
38
|
|
|
|
|
|
|
], |
|
39
|
|
|
|
|
|
|
[ 'o=s', 'Output the results to this file instead of printing.', { 'completion' => 'files' } ], |
|
40
|
|
|
|
|
|
|
[ 'w', 'If the file specified via -o exists, over write it.' ], |
|
41
|
|
|
|
|
|
|
[ 'd', 'Include the input data in the output (CSV input only).' ], |
|
42
|
|
|
|
|
|
|
[ |
|
43
|
|
|
|
|
|
|
'mode=s', |
|
44
|
|
|
|
|
|
|
"What each row does: 'prequential' (score against the model as it stood, then learn -- the " |
|
45
|
|
|
|
|
|
|
. "default), 'learn' (learn only, no output), or 'score' (score only, nothing learned).", |
|
46
|
|
|
|
|
|
|
{ 'default' => 'prequential' } |
|
47
|
|
|
|
|
|
|
], |
|
48
|
|
|
|
|
|
|
[ |
|
49
|
|
|
|
|
|
|
'jsonl', |
|
50
|
|
|
|
|
|
|
'Input lines are JSON rows instead of CSV: an array is positional, an object is a tagged row ' |
|
51
|
|
|
|
|
|
|
. '(full munger plan, raw values may contain anything JSON can). Output is the daemon\'s ' |
|
52
|
|
|
|
|
|
|
. 'reply JSON lines verbatim, one per request (--batch 1 for one per row).' |
|
53
|
|
|
|
|
|
|
], |
|
54
|
|
|
|
|
|
|
[ |
|
55
|
|
|
|
|
|
|
'batch=i', |
|
56
|
|
|
|
|
|
|
'Rows per request message. Bigger amortises round trips; 1 gives per-row latency for ' |
|
57
|
|
|
|
|
|
|
. 'tail -F style pipelines.', |
|
58
|
|
|
|
|
|
|
{ 'default' => 256 } |
|
59
|
|
|
|
|
|
|
], |
|
60
|
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
# command mode |
|
62
|
|
|
|
|
|
|
[ 'ping', 'Check the daemon is alive; exits 0 on pong.' ], |
|
63
|
|
|
|
|
|
|
[ 'stats', 'Print the daemon stats (seen, window, threshold, connections, set, ...).' ], |
|
64
|
|
|
|
|
|
|
[ 'save', 'Ask the daemon to save the model now; prints the file name.' ], |
|
65
|
|
|
|
|
|
|
[ 'relearn-threshold', 'Ask the daemon to relearn the contamination decision threshold.' ], |
|
66
|
|
|
|
|
|
|
[ 'json', 'Command mode: print the raw JSON reply instead of the text rendering.' ], |
|
67
|
|
|
|
|
|
|
); |
|
68
|
|
|
|
|
|
|
} ## end sub opt_spec |
|
69
|
|
|
|
|
|
|
|
|
70
|
0
|
|
|
0
|
1
|
0
|
sub abstract { 'Client for iforest streamd: stream rows through it or send it commands' } |
|
71
|
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
sub description { |
|
73
|
0
|
|
|
0
|
1
|
0
|
'Talks to a running `iforest streamd` daemon over its Unix socket, |
|
74
|
|
|
|
|
|
|
speaking the same one-JSON-document-per-line protocol. |
|
75
|
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
Stream mode (-i) feeds rows through the daemon and prints one result |
|
77
|
|
|
|
|
|
|
per row, in order. Input is CSV by default (positional rows, matching |
|
78
|
|
|
|
|
|
|
`iforest stream`; fields are sent as numbers when they look like |
|
79
|
|
|
|
|
|
|
numbers and as raw strings otherwise, so munged columns pass through |
|
80
|
|
|
|
|
|
|
untouched) and the output is `$score,$label` lines, with -d prepending |
|
81
|
|
|
|
|
|
|
the input columns. With --jsonl each input line is instead a JSON row |
|
82
|
|
|
|
|
|
|
-- an array for positional data, an object for a tagged row through |
|
83
|
|
|
|
|
|
|
the full munger plan -- and the output is the daemon\'s reply JSON |
|
84
|
|
|
|
|
|
|
lines verbatim. Rows are sent in --batch sized messages, lockstep; |
|
85
|
|
|
|
|
|
|
each request is tagged with its starting input line number, so a bad |
|
86
|
|
|
|
|
|
|
row dies naming the input line (rows earlier in that message were |
|
87
|
|
|
|
|
|
|
already applied by the daemon -- prequential learning is not |
|
88
|
|
|
|
|
|
|
transactional). All row validation is the daemon\'s: only it knows |
|
89
|
|
|
|
|
|
|
whether the model munges. |
|
90
|
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
Command mode sends exactly one of --ping, --stats, --save, or |
|
92
|
|
|
|
|
|
|
--relearn-threshold and renders the reply as text (--json for the raw |
|
93
|
|
|
|
|
|
|
reply). The exit code is 0 on ok and non-zero on error, connect |
|
94
|
|
|
|
|
|
|
failure, or timeout, so `iforest streamc --set web --ping` works |
|
95
|
|
|
|
|
|
|
directly in health checks. |
|
96
|
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
--set/--socket resolve the socket path exactly as streamd does, so the |
|
98
|
|
|
|
|
|
|
same flags reach the same daemon. |
|
99
|
|
|
|
|
|
|
'; |
|
100
|
|
|
|
|
|
|
} ## end sub description |
|
101
|
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
sub validate { |
|
103
|
23
|
|
|
23
|
0
|
67
|
my ( $self, $opt, $args ) = @_; |
|
104
|
|
|
|
|
|
|
|
|
105
|
23
|
100
|
100
|
|
|
3877
|
if ( defined( $opt->{'set'} ) && $opt->{'set'} !~ /\A[A-Za-z0-9+\-@_]+\z/ ) { |
|
106
|
|
|
|
|
|
|
$self->usage_error( '--set, "' |
|
107
|
1
|
|
|
|
|
17
|
. $opt->{'set'} |
|
108
|
|
|
|
|
|
|
. '", must match /\A[A-Za-z0-9+\-@_]+\z/ (letters, digits, and + - @ _ only)' ); |
|
109
|
|
|
|
|
|
|
} |
|
110
|
|
|
|
|
|
|
|
|
111
|
22
|
|
|
|
|
91
|
my @cmds = grep { $opt->{$_} } qw(ping stats save relearn_threshold); |
|
|
88
|
|
|
|
|
212
|
|
|
112
|
22
|
100
|
|
|
|
121
|
if ( defined( $opt->{'i'} ) ) { |
|
|
|
100
|
|
|
|
|
|
|
113
|
10
|
50
|
|
|
|
36
|
if ( scalar @cmds ) { |
|
114
|
0
|
|
|
|
|
0
|
$self->usage_error('-i may not be combined with --ping/--stats/--save/--relearn-threshold'); |
|
115
|
|
|
|
|
|
|
} |
|
116
|
|
|
|
|
|
|
} elsif ( scalar @cmds != 1 ) { |
|
117
|
2
|
|
|
|
|
17
|
$self->usage_error( |
|
118
|
|
|
|
|
|
|
'need either -i (stream mode) or exactly one of --ping, --stats, --save, --relearn-threshold'); |
|
119
|
|
|
|
|
|
|
} |
|
120
|
|
|
|
|
|
|
|
|
121
|
20
|
100
|
66
|
|
|
132
|
if ( defined( $opt->{'i'} ) && $opt->{'i'} ne '-' ) { |
|
122
|
10
|
50
|
|
|
|
387
|
if ( !-f $opt->{'i'} ) { |
|
|
|
50
|
|
|
|
|
|
|
123
|
0
|
|
|
|
|
0
|
$self->usage_error( '-i, "' . $opt->{'i'} . '", is not a file or does not exist' ); |
|
124
|
|
|
|
|
|
|
} elsif ( !-r $opt->{'i'} ) { |
|
125
|
0
|
|
|
|
|
0
|
$self->usage_error( '-i, "' . $opt->{'i'} . '", is not readable' ); |
|
126
|
|
|
|
|
|
|
} |
|
127
|
|
|
|
|
|
|
} |
|
128
|
|
|
|
|
|
|
|
|
129
|
20
|
50
|
66
|
|
|
125
|
if ( defined( $opt->{'o'} ) && !$opt->{'w'} && -e $opt->{'o'} ) { |
|
|
|
|
66
|
|
|
|
|
|
130
|
0
|
|
|
|
|
0
|
$self->usage_error( '-o, "' . $opt->{'o'} . '", already exists and -w is not specified' ); |
|
131
|
|
|
|
|
|
|
} |
|
132
|
|
|
|
|
|
|
|
|
133
|
20
|
50
|
|
|
|
126
|
if ( $opt->{'mode'} !~ /\A(?:prequential|learn|score)\z/ ) { |
|
134
|
0
|
|
|
|
|
0
|
$self->usage_error( '--mode, "' . $opt->{'mode'} . '", must be prequential, learn, or score' ); |
|
135
|
|
|
|
|
|
|
} |
|
136
|
|
|
|
|
|
|
|
|
137
|
20
|
100
|
100
|
|
|
70
|
if ( $opt->{'d'} && $opt->{'jsonl'} ) { |
|
138
|
1
|
|
|
|
|
8
|
$self->usage_error('-d only applies to CSV input; --jsonl replies are already self-describing'); |
|
139
|
|
|
|
|
|
|
} |
|
140
|
|
|
|
|
|
|
|
|
141
|
19
|
50
|
66
|
|
|
99
|
if ( $opt->{'json'} && defined( $opt->{'i'} ) ) { |
|
142
|
0
|
|
|
|
|
0
|
$self->usage_error('--json only applies to command mode'); |
|
143
|
|
|
|
|
|
|
} |
|
144
|
|
|
|
|
|
|
|
|
145
|
19
|
50
|
|
|
|
94
|
if ( $opt->{'batch'} < 1 ) { |
|
146
|
0
|
|
|
|
|
0
|
$self->usage_error( '--batch, "' . $opt->{'batch'} . '", must be >= 1' ); |
|
147
|
|
|
|
|
|
|
} |
|
148
|
|
|
|
|
|
|
|
|
149
|
19
|
50
|
|
|
|
58
|
if ( $opt->{'timeout'} < 1 ) { |
|
150
|
0
|
|
|
|
|
0
|
$self->usage_error( '--timeout, "' . $opt->{'timeout'} . '", must be >= 1' ); |
|
151
|
|
|
|
|
|
|
} |
|
152
|
|
|
|
|
|
|
|
|
153
|
19
|
|
|
|
|
72
|
return 1; |
|
154
|
|
|
|
|
|
|
} ## end sub validate |
|
155
|
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
sub execute { |
|
157
|
19
|
|
|
19
|
1
|
122
|
my ( $self, $opt, $args ) = @_; |
|
158
|
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
# Lazily required for the same reason streamd does it: App::Cmd loads |
|
160
|
|
|
|
|
|
|
# every command module up front, and the rest of the CLI should work |
|
161
|
|
|
|
|
|
|
# on a box without JSON::MaybeXS. |
|
162
|
19
|
50
|
|
|
|
43
|
eval { require JSON::MaybeXS; 1 } |
|
|
19
|
|
|
|
|
7901
|
|
|
|
19
|
|
|
|
|
107291
|
|
|
163
|
|
|
|
|
|
|
or die( 'iforest streamc requires JSON::MaybeXS for its wire protocol; install it: ' . $@ ); |
|
164
|
19
|
|
|
|
|
99
|
$JSON = JSON::MaybeXS->new( utf8 => 1, canonical => 1 ); |
|
165
|
19
|
|
|
|
|
403
|
$TIMEOUT = $opt->{'timeout'}; |
|
166
|
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
# Resolve the socket exactly as streamd does (keep in sync with it): |
|
168
|
|
|
|
|
|
|
# without a set the flag is the socket file; with one it is the base |
|
169
|
|
|
|
|
|
|
# run dir holding .sock. |
|
170
|
19
|
|
|
|
|
40
|
my $socket; |
|
171
|
19
|
100
|
|
|
|
84
|
if ( defined $opt->{'set'} ) { |
|
172
|
18
|
50
|
|
|
|
111
|
my $run = defined $opt->{'socket'} ? $opt->{'socket'} : '/var/run/iforest_streamd'; |
|
173
|
18
|
|
|
|
|
337
|
$socket = File::Spec->catfile( $run, $opt->{'set'} . '.sock' ); |
|
174
|
|
|
|
|
|
|
} else { |
|
175
|
1
|
50
|
|
|
|
3
|
$socket = defined $opt->{'socket'} ? $opt->{'socket'} : '/var/run/iforest_streamd/streamd.sock'; |
|
176
|
|
|
|
|
|
|
} |
|
177
|
19
|
50
|
|
|
|
98
|
die( '--socket, "' |
|
178
|
|
|
|
|
|
|
. $socket |
|
179
|
|
|
|
|
|
|
. '", is ' |
|
180
|
|
|
|
|
|
|
. length($socket) |
|
181
|
|
|
|
|
|
|
. ' bytes; Unix socket paths are limited to ~104 bytes -- use a shorter path' |
|
182
|
|
|
|
|
|
|
. "\n" ) |
|
183
|
|
|
|
|
|
|
if length($socket) > 100; |
|
184
|
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
$SOCK = IO::Socket::UNIX->new( Peer => $socket ) |
|
186
|
|
|
|
|
|
|
or die( 'failed to connect to "' |
|
187
|
|
|
|
|
|
|
. $socket . '": ' |
|
188
|
|
|
|
|
|
|
. $! |
|
189
|
|
|
|
|
|
|
. ' -- is streamd running' |
|
190
|
19
|
50
|
|
|
|
185
|
. ( defined $opt->{'set'} ? ' with --set ' . $opt->{'set'} : '' ) . '?' |
|
|
|
100
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
. "\n" ); |
|
192
|
18
|
|
|
|
|
5034
|
$SOCK->autoflush(1); |
|
193
|
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
# A daemon dropping us mid-write should surface as the read-side |
|
195
|
|
|
|
|
|
|
# "no reply" error, not a silent SIGPIPE death. |
|
196
|
18
|
|
|
|
|
913
|
local $SIG{PIPE} = 'IGNORE'; |
|
197
|
|
|
|
|
|
|
|
|
198
|
18
|
100
|
|
|
|
132
|
return _command( $self, $opt ) if !defined $opt->{'i'}; |
|
199
|
9
|
|
|
|
|
45
|
return _stream( $self, $opt ); |
|
200
|
|
|
|
|
|
|
} ## end sub execute |
|
201
|
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
203
|
|
|
|
|
|
|
# wire helpers |
|
204
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
205
|
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
sub _request { |
|
207
|
20
|
|
|
20
|
|
73
|
my ($msg) = @_; |
|
208
|
20
|
|
|
|
|
42
|
print {$SOCK} $JSON->encode($msg) . "\n"; |
|
|
20
|
|
|
|
|
860
|
|
|
209
|
20
|
|
|
|
|
92
|
my $reply = _read_reply(); |
|
210
|
20
|
50
|
|
|
|
78
|
die( 'no reply from the daemon within ' . $TIMEOUT . 's (or it closed the connection)' . "\n" ) |
|
211
|
|
|
|
|
|
|
unless defined $reply; |
|
212
|
20
|
|
|
|
|
62
|
return $reply; |
|
213
|
|
|
|
|
|
|
} |
|
214
|
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
sub _read_reply { |
|
216
|
20
|
|
|
20
|
|
57
|
my $deadline = time + $TIMEOUT; |
|
217
|
20
|
|
|
|
|
165
|
my $sel = IO::Select->new($SOCK); |
|
218
|
20
|
|
|
|
|
1378
|
while ( $READ_BUF !~ /\n/ ) { |
|
219
|
20
|
|
|
|
|
53
|
my $left = $deadline - time; |
|
220
|
20
|
50
|
33
|
|
|
140
|
return undef if $left <= 0 || !$sel->can_read($left); |
|
221
|
20
|
|
|
|
|
200072
|
my $got = sysread( $SOCK, my $chunk, 65536 ); |
|
222
|
20
|
50
|
|
|
|
165
|
return undef unless $got; |
|
223
|
20
|
|
|
|
|
179
|
$READ_BUF .= $chunk; |
|
224
|
|
|
|
|
|
|
} |
|
225
|
20
|
|
|
|
|
195
|
$READ_BUF =~ s/\A([^\n]*)\n//; |
|
226
|
20
|
|
|
|
|
96
|
my $line = $1; |
|
227
|
20
|
|
|
|
|
56
|
my $reply = eval { $JSON->decode($line) }; |
|
|
20
|
|
|
|
|
334
|
|
|
228
|
20
|
50
|
|
|
|
84
|
die( 'daemon sent an unparseable reply: ' . $@ ) if $@; |
|
229
|
20
|
|
|
|
|
308
|
return { raw => $line, reply => $reply }; |
|
230
|
|
|
|
|
|
|
} ## end sub _read_reply |
|
231
|
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
233
|
|
|
|
|
|
|
# command mode |
|
234
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
235
|
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
sub _command { |
|
237
|
9
|
|
|
9
|
|
23
|
my ( $self, $opt ) = @_; |
|
238
|
|
|
|
|
|
|
|
|
239
|
9
|
|
|
|
|
26
|
my ($which) = grep { $opt->{$_} } qw(ping stats save relearn_threshold); |
|
|
36
|
|
|
|
|
80
|
|
|
240
|
9
|
|
|
|
|
28
|
( my $cmd = $which ) =~ tr/_/-/; |
|
241
|
|
|
|
|
|
|
|
|
242
|
9
|
|
|
|
|
46
|
my $got = _request( { cmd => $cmd } ); |
|
243
|
9
|
|
|
|
|
36
|
my $reply = $got->{reply}; |
|
244
|
9
|
100
|
|
|
|
229
|
die( 'daemon error: ' . $reply->{error} . "\n" ) if defined $reply->{error}; |
|
245
|
|
|
|
|
|
|
|
|
246
|
8
|
100
|
|
|
|
59
|
if ( $opt->{'json'} ) { |
|
247
|
1
|
|
|
|
|
14
|
print $got->{raw} . "\n"; |
|
248
|
1
|
|
|
|
|
164
|
return 1; |
|
249
|
|
|
|
|
|
|
} |
|
250
|
|
|
|
|
|
|
|
|
251
|
7
|
|
|
|
|
18
|
my $ok = $reply->{ok}; |
|
252
|
7
|
100
|
|
|
|
30
|
if ( ref $ok eq 'HASH' ) { |
|
253
|
5
|
|
|
|
|
30
|
for my $k ( sort keys %$ok ) { |
|
254
|
29
|
100
|
|
|
|
198
|
printf " %-20s %s\n", $k, ( defined $ok->{$k} ? $ok->{$k} : '(unset)' ); |
|
255
|
|
|
|
|
|
|
} |
|
256
|
|
|
|
|
|
|
} else { |
|
257
|
2
|
50
|
|
|
|
32
|
print( ( defined $ok ? $ok : 'ok' ) . "\n" ); |
|
258
|
|
|
|
|
|
|
} |
|
259
|
7
|
|
|
|
|
1057
|
return 1; |
|
260
|
|
|
|
|
|
|
} ## end sub _command |
|
261
|
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
263
|
|
|
|
|
|
|
# stream mode |
|
264
|
|
|
|
|
|
|
#------------------------------------------------------------------------------- |
|
265
|
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
sub _stream { |
|
267
|
9
|
|
|
9
|
|
28
|
my ( $self, $opt ) = @_; |
|
268
|
|
|
|
|
|
|
|
|
269
|
9
|
|
|
|
|
20
|
my $in_fh; |
|
270
|
9
|
50
|
|
|
|
55
|
if ( $opt->{'i'} eq '-' ) { |
|
271
|
0
|
|
|
|
|
0
|
$in_fh = \*STDIN; |
|
272
|
|
|
|
|
|
|
} else { |
|
273
|
9
|
50
|
|
|
|
408
|
open( $in_fh, '<', $opt->{'i'} ) or die( 'failed to open -i, "' . $opt->{'i'} . '": ' . $! . "\n" ); |
|
274
|
|
|
|
|
|
|
} |
|
275
|
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
# -o accumulates and writes atomically at the end (matching `iforest |
|
277
|
|
|
|
|
|
|
# stream`), so it is unsuitable for an endless stdin; without it, |
|
278
|
|
|
|
|
|
|
# results print as replies arrive. |
|
279
|
9
|
|
|
|
|
32
|
my $results = ''; |
|
280
|
|
|
|
|
|
|
my $emit = sub { |
|
281
|
27
|
100
|
|
27
|
|
64
|
if ( defined $opt->{'o'} ) { $results .= $_[0] . "\n" } |
|
|
5
|
|
|
|
|
12
|
|
|
282
|
22
|
|
|
|
|
138
|
else { print $_[0] . "\n" } |
|
283
|
9
|
|
|
|
|
56
|
}; |
|
284
|
|
|
|
|
|
|
|
|
285
|
9
|
|
|
|
|
38
|
my $expected_cols; |
|
286
|
|
|
|
|
|
|
my @rows; # decoded rows for the pending request |
|
287
|
9
|
|
|
|
|
0
|
my @raw; # matching raw input lines, for -d |
|
288
|
9
|
|
|
|
|
18
|
my $batch_start = 1; # input line number of $rows[0] |
|
289
|
9
|
|
|
|
|
21
|
my $line_int = 0; |
|
290
|
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
my $flush = sub { |
|
292
|
12
|
100
|
|
12
|
|
51
|
return unless @rows; |
|
293
|
11
|
|
|
|
|
85
|
my $got = _request( { rows => [@rows], mode => $opt->{'mode'}, tag => $batch_start } ); |
|
294
|
11
|
|
|
|
|
60
|
my $reply = $got->{reply}; |
|
295
|
11
|
100
|
|
|
|
42
|
if ( defined $reply->{error} ) { |
|
296
|
1
|
|
|
|
|
3
|
my $line = $batch_start; |
|
297
|
1
|
|
|
|
|
3
|
my $err = $reply->{error}; |
|
298
|
|
|
|
|
|
|
# Batch errors come back as "row N: ..." with N relative to |
|
299
|
|
|
|
|
|
|
# the message; map it back to the input line. |
|
300
|
1
|
50
|
|
|
|
32
|
if ( $err =~ s/\Arow (\d+): // ) { |
|
301
|
1
|
|
|
|
|
7
|
$line = $batch_start + $1; |
|
302
|
|
|
|
|
|
|
} |
|
303
|
1
|
|
|
|
|
329
|
die( 'line ' . $line . ' of input: ' . $err . "\n" ); |
|
304
|
|
|
|
|
|
|
} ## end if ( defined $reply->{error} ) |
|
305
|
10
|
100
|
|
|
|
75
|
if ( $opt->{'mode'} ne 'learn' ) { |
|
306
|
9
|
100
|
|
|
|
32
|
if ( $opt->{'jsonl'} ) { |
|
307
|
2
|
|
|
|
|
8
|
$emit->( $got->{raw} ); |
|
308
|
|
|
|
|
|
|
} else { |
|
309
|
7
|
|
|
|
|
13
|
my $pairs = $reply->{scores}; |
|
310
|
7
|
|
|
|
|
32
|
for my $i ( 0 .. $#$pairs ) { |
|
311
|
25
|
100
|
|
|
|
69
|
my $prefix = $opt->{'d'} ? $raw[$i] . ',' : ''; |
|
312
|
25
|
|
|
|
|
143
|
$emit->( $prefix . $pairs->[$i][0] . ',' . $pairs->[$i][1] ); |
|
313
|
|
|
|
|
|
|
} |
|
314
|
|
|
|
|
|
|
} |
|
315
|
|
|
|
|
|
|
} ## end if ( $opt->{'mode'} ne 'learn' ) |
|
316
|
10
|
|
|
|
|
42
|
@rows = (); |
|
317
|
10
|
|
|
|
|
34
|
@raw = (); |
|
318
|
10
|
|
|
|
|
75
|
$batch_start = $line_int + 1; |
|
319
|
9
|
|
|
|
|
47
|
}; ## end $flush = sub |
|
320
|
|
|
|
|
|
|
|
|
321
|
9
|
|
|
|
|
235
|
while ( my $line = <$in_fh> ) { |
|
322
|
92
|
|
|
|
|
131
|
$line_int++; |
|
323
|
92
|
|
|
|
|
126
|
chomp $line; |
|
324
|
92
|
50
|
|
|
|
239
|
if ( $line =~ /^\s*$/ ) { |
|
325
|
0
|
|
|
|
|
0
|
$flush->(); # keep line-number accounting exact across blanks |
|
326
|
0
|
|
|
|
|
0
|
$batch_start = $line_int + 1; |
|
327
|
0
|
|
|
|
|
0
|
next; |
|
328
|
|
|
|
|
|
|
} |
|
329
|
|
|
|
|
|
|
|
|
330
|
92
|
100
|
|
|
|
200
|
if ( $opt->{'jsonl'} ) { |
|
331
|
4
|
|
|
|
|
14
|
my $row = eval { $JSON->decode($line) }; |
|
|
4
|
|
|
|
|
62
|
|
|
332
|
4
|
50
|
|
|
|
15
|
die( 'line ' . $line_int . ' of -i did not parse as JSON: ' . $@ ) if $@; |
|
333
|
4
|
50
|
33
|
|
|
58
|
die( 'line ' . $line_int . ' of -i must be a JSON array (positional) or object (tagged)' . "\n" ) |
|
334
|
|
|
|
|
|
|
unless ref $row eq 'ARRAY' || ref $row eq 'HASH'; |
|
335
|
4
|
|
|
|
|
12
|
push @rows, $row; |
|
336
|
|
|
|
|
|
|
} else { |
|
337
|
88
|
|
|
|
|
172
|
my @fields = split( /,/, $line, -1 ); |
|
338
|
88
|
100
|
|
|
|
159
|
if ( !defined $expected_cols ) { |
|
|
|
100
|
|
|
|
|
|
|
339
|
7
|
|
|
|
|
65
|
$expected_cols = scalar @fields; |
|
340
|
7
|
50
|
|
|
|
37
|
die( 'Line ' . $line_int . ' of input has no columns' ) if $expected_cols < 1; |
|
341
|
|
|
|
|
|
|
} elsif ( scalar @fields != $expected_cols ) { |
|
342
|
1
|
|
|
|
|
188
|
die( 'Line ' |
|
343
|
|
|
|
|
|
|
. $line_int |
|
344
|
|
|
|
|
|
|
. ' of input has ' |
|
345
|
|
|
|
|
|
|
. scalar(@fields) |
|
346
|
|
|
|
|
|
|
. ' columns but expected ' |
|
347
|
|
|
|
|
|
|
. $expected_cols ); |
|
348
|
|
|
|
|
|
|
} |
|
349
|
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
# Numeric-looking fields travel as JSON numbers, everything |
|
351
|
|
|
|
|
|
|
# else as strings for the daemon's munger plan to handle; the |
|
352
|
|
|
|
|
|
|
# daemon owns validation either way. |
|
353
|
87
|
50
|
|
|
|
120
|
push @rows, [ map { looks_like_number($_) ? 0 + $_ : $_ } @fields ]; |
|
|
174
|
|
|
|
|
497
|
|
|
354
|
87
|
|
|
|
|
171
|
push @raw, $line; |
|
355
|
|
|
|
|
|
|
} ## end else [ if ( $opt->{'jsonl'} ) ] |
|
356
|
|
|
|
|
|
|
|
|
357
|
91
|
100
|
|
|
|
314
|
$flush->() if scalar @rows >= $opt->{'batch'}; |
|
358
|
|
|
|
|
|
|
} ## end while ( my $line = <$in_fh> ) |
|
359
|
8
|
|
|
|
|
40
|
$flush->(); |
|
360
|
|
|
|
|
|
|
|
|
361
|
7
|
100
|
|
|
|
27
|
if ( defined $opt->{'o'} ) { |
|
362
|
1
|
|
|
|
|
16
|
write_file( $opt->{'o'}, { 'atomic' => 1 }, $results ); |
|
363
|
|
|
|
|
|
|
} |
|
364
|
7
|
|
|
|
|
2418
|
return 1; |
|
365
|
|
|
|
|
|
|
} ## end sub _stream |
|
366
|
|
|
|
|
|
|
|
|
367
|
|
|
|
|
|
|
return 1; |