File Coverage

blib/lib/Mail/SpamAssassin/Plugin/RuleTimingRedis.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Mail::SpamAssassin::Plugin::RuleTimingRedis;
2              
3 1     1   1425 use Mail::SpamAssassin::Plugin;
  0            
  0            
4             use Mail::SpamAssassin::Logger;
5             use Mail::SpamAssassin::Util qw( untaint_var );
6              
7             use strict;
8             use warnings;
9              
10             # ABSTRACT: collect SA rule timings in redis
11             our $VERSION = '1.004'; # VERSION
12              
13              
14             use Time::HiRes qw(time);
15              
16             use vars qw(@ISA);
17             @ISA = qw(Mail::SpamAssassin::Plugin);
18              
19             use Redis;
20              
21             our $BULK_SCRIPT = <<"EOT";
22             for i=1,#KEYS do
23             redis.call('INCR', KEYS[i] .. ".count" )
24             redis.call('INCRBY', KEYS[i] .. ".time", ARGV[i] )
25             end
26             return #KEYS
27             EOT
28              
29             sub new {
30             my $class = shift;
31             my $mailsaobject = shift;
32              
33             $class = ref($class) || $class;
34             my $self = $class->SUPER::new($mailsaobject);
35             bless ($self, $class);
36              
37             $mailsaobject->{conf}->{parser}->register_commands( [
38             {
39             setting => 'timing_redis_server',
40             is_admin => 1,
41             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
42             default => '127.0.0.1:6379',
43             }, {
44             setting => 'timing_redis_password',
45             is_admin => 1,
46             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
47             }, {
48             setting => 'timing_redis_exclude_re',
49             is_admin => 1,
50             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
51             default => '^__',
52             }, {
53             setting => 'timing_redis_prefix',
54             is_admin => 1,
55             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
56             default => 'sa-timing.',
57             }, {
58             setting => 'timing_redis_database',
59             is_admin => 1,
60             type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
61             default => 0,
62             }, {
63             setting => 'timing_redis_precision',
64             is_admin => 1,
65             type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
66             default => 1000000, # microseconds (millionths of a second)
67             }, {
68             setting => 'timing_redis_bulk_update',
69             is_admin => 1,
70             type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
71             default => 50,
72             }, {
73             setting => 'timing_redis_debug',
74             is_admin => 1,
75             type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
76             default => 0,
77             },
78             ] );
79              
80             return( $self );
81             }
82              
83             sub _get_redis {
84             my $self = shift;
85             my $conf = $self->{main}->{conf};
86             my ( $server, $debug, $password, $database, $bulk ) =
87             @$conf{ 'timing_redis_server','timing_redis_debug', 'timing_redis_password',
88             'timing_redis_database', 'timing_redis_bulk_update' };
89              
90             untaint_var( \$server );
91              
92             if( ! defined $self->{'_redis'} ) {
93             Mail::SpamAssassin::Plugin::info('initializing connection to redis server...');
94             eval {
95             $self->{'_redis'} = Redis->new(
96             'server' => $server,
97             'debug' => $debug,
98             defined $password ? ( password => $password ) : (),
99             );
100             };
101             if( $@ ) {
102             die('could not connect to redis: '.$@);
103             }
104             if( $database ) {
105             Mail::SpamAssassin::Plugin::info("selecting redis database $database...");
106             $self->{'_redis'}->select($database);
107             }
108             if( $bulk ) {
109             Mail::SpamAssassin::Plugin::info("loading redis lua bulk script...");
110             $self->{'_script'} = $self->{'_redis'}->script_load($BULK_SCRIPT);
111             Mail::SpamAssassin::Plugin::dbg("script loaded as ".$self->{'_script'});
112             }
113             }
114             return $self->{'_redis'};
115             }
116              
117             sub _flush_queue {
118             my ( $self, $queue ) = @_;
119             my $prefix = $self->{main}->{conf}->{'timing_redis_prefix'};
120              
121             my $count = scalar @$queue;
122             if( ! $count ) {
123             return;
124             }
125             Mail::SpamAssassin::Plugin::dbg("flushing $count timing events to redis...");
126             my @args;
127             push( @args, map { $prefix.$_->[0] } @$queue );
128             push( @args, map { $_->[1] } @$queue );
129              
130             $self->{'_redis'}->evalsha(
131             $self->{'_script'}, $count, @args, sub {});
132              
133             @$queue = ();
134              
135             return;
136             }
137              
138             sub check_start {
139             my ($self, $options) = @_;
140             $options->{permsgstatus}->{'rule_timing_queue'} = [];
141             return;
142             }
143              
144             sub start_rules {
145             my ($self, $options) = @_;
146             $options->{permsgstatus}->{'rule_timing_start'} = Time::HiRes::time();
147             return;
148             }
149              
150             sub ran_rule {
151             my $time = Time::HiRes::time();
152             my ($self, $options) = @_;
153             my $exclude_re = $self->{main}->{conf}->{'timing_redis_exclude_re'};
154             my $bulk = $self->{main}->{conf}->{'timing_redis_bulk_update'};
155             my $queue = $options->{permsgstatus}->{'rule_timing_queue'};
156              
157             my $permsg = $options->{permsgstatus};
158             my $name = $options->{rulename};
159             if( defined $exclude_re
160             && $exclude_re ne ''
161             && $name =~ /$exclude_re/ ) {
162             $permsg->{'rule_timing_start'} = Time::HiRes::time();
163             return;
164             }
165             my $prefix = $self->{main}->{conf}->{'timing_redis_prefix'};
166             my $precision = $self->{main}->{conf}->{'timing_redis_precision'};
167              
168             my $duration = int(($time - $permsg->{'rule_timing_start'}) * $precision);
169              
170             my $redis = $self->_get_redis;
171              
172             if( $bulk ) {
173             push( @$queue, [ $name, $duration ] );
174              
175             if( scalar @$queue >= $bulk ) {
176             $self->_flush_queue( $queue );
177             }
178             } else {
179             $redis->incrby($prefix.$name.'.time', $duration, sub {} );
180             $redis->incr($prefix.$name.'.count', sub {} );
181             }
182              
183             $permsg->{'rule_timing_start'} = Time::HiRes::time();
184             return;
185             }
186              
187             sub check_end {
188             my ($self, $options) = @_;
189             my $bulk = $self->{main}->{conf}->{'timing_redis_bulk_update'};
190             my $queue = $options->{permsgstatus}->{'rule_timing_queue'};
191              
192             my $redis = $self->_get_redis;
193             if( $bulk ) {
194             Mail::SpamAssassin::Plugin::dbg("cleaning up redis timing queue (".scalar(@$queue)." left)...");
195             $self->_flush_queue( $queue );
196             }
197             Mail::SpamAssassin::Plugin::dbg("waiting for redis pipelined responses...");
198             $redis->wait_all_responses;
199              
200             return;
201             }
202              
203             sub finish {
204             my $self = shift;
205             if( defined $self->{'redis'} ) {
206             $self->{'redis'}->quit;
207             }
208             return;
209             }
210              
211             1;
212              
213             __END__