File Coverage

blib/lib/Mail/SpamAssassin/Timeout.pm
Criterion Covered Total %
statement 108 112 96.4
branch 50 58 86.2
condition 18 21 85.7
subroutine 13 13 100.0
pod 5 5 100.0
total 194 209 92.8


line stmt bran cond sub pod time code
1             # <@LICENSE>
2             # Licensed to the Apache Software Foundation (ASF) under one or more
3             # contributor license agreements. See the NOTICE file distributed with
4             # this work for additional information regarding copyright ownership.
5             # The ASF licenses this file to you under the Apache License, Version 2.0
6             # (the "License"); you may not use this file except in compliance with
7             # the License. You may obtain a copy of the License at:
8             #
9             # http://www.apache.org/licenses/LICENSE-2.0
10             #
11             # Unless required by applicable law or agreed to in writing, software
12             # distributed under the License is distributed on an "AS IS" BASIS,
13             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14             # See the License for the specific language governing permissions and
15             # limitations under the License.
16             # </@LICENSE>
17              
18             =head1 NAME
19              
20             Mail::SpamAssassin::Timeout - safe, reliable timeouts in perl
21              
22             =head1 SYNOPSIS
23              
24             # non-timeout code...
25              
26             my $t = Mail::SpamAssassin::Timeout->new({ secs => 5, deadline => $when });
27            
28             $t->run(sub {
29             # code to run with a 5-second timeout...
30             });
31              
32             if ($t->timed_out()) {
33             # do something...
34             }
35              
36             # more non-timeout code...
37              
38             =head1 DESCRIPTION
39              
40             This module provides a safe, reliable and clean API to provide
41             C<alarm(2)>-based timeouts for perl code.
42              
43             Note that C<$SIG{ALRM}> is used to provide the timeout, so this will not
44             interrupt out-of-control regular expression matches.
45              
46             Nested timeouts are supported.
47              
48             =head1 PUBLIC METHODS
49              
50             =over 4
51              
52             =cut
53              
54              
55             use strict;
56 42     42   24622 use warnings;
  42         83  
  42         1240  
57 42     42   211 # use bytes;
  42         71  
  42         1530  
58             use re 'taint';
59 42     42   255  
  42         99  
  42         1513  
60             use Time::HiRes qw(time);
61 42     42   216 use Mail::SpamAssassin::Logger;
  42         92  
  42         3137  
62 42     42   4902  
  42         73  
  42         3576  
63             our @ISA = qw();
64              
65             ###########################################################################
66              
67             =item my $t = Mail::SpamAssassin::Timeout->new({ ... options ... });
68              
69             Constructor. Options include:
70              
71             =over 4
72              
73             =item secs => $seconds
74              
75             time interval, in seconds. Optional; if neither C<secs> nor C<deadline> is
76             specified, no timeouts will be applied.
77              
78             =item deadline => $unix_timestamp
79              
80             Unix timestamp (seconds since epoch) when a timeout is reached in the latest.
81             Optional; if neither B<secs> nor B<deadline> is specified, no timeouts will
82             be applied. If both are specified, the shorter interval of the two prevails.
83              
84             =back
85              
86             =cut
87              
88             our $id_gen;
89             BEGIN { $id_gen = 0 } # unique generator of IDs for timer objects
90 42     42   36580 our @expiration; # stack of expected expiration times, top at [0]
91              
92             my ($class, $opts) = @_;
93             $class = ref($class) || $class;
94 2025     2025 1 19286 my %selfval = $opts ? %{$opts} : ();
95 2025   33     5192 $selfval{id} = ++$id_gen;
96 2025 100       3431 my($package, $filename, $line, $subroutine) = caller(1);
  2022         6425  
97 2025         4289 if (defined $subroutine) {
98 2025         12077 $subroutine =~ s/^Mail::SpamAssassin::/::/;
99 2025 100       4940 $selfval{id} = join('/', $id_gen, $subroutine, $line);
100 2000         7715 }
101 2000         5164 my $self = \%selfval;
102              
103 2025         2912 bless ($self, $class);
104             $self;
105 2025         3057 }
106 2025         5344  
107             ###########################################################################
108              
109             =item $t->run($coderef)
110              
111             Run a code reference within the currently-defined timeout.
112              
113             The timeout is as defined by the B<secs> and B<deadline> parameters
114             to the constructor.
115              
116             Returns whatever the subroutine returns, or C<undef> on timeout.
117             If the timer times out, C<$t-<gt>timed_out()> will return C<1>.
118              
119             Time elapsed is not cumulative; multiple runs of C<run> will restart the
120             timeout from scratch. On the other hand, nested timers do observe outer
121             timeouts if they are shorter, resignalling a timeout to the level which
122             established them, i.e. code running under an inner timer can not exceed
123             the time limit established by an outer timer. When restarting an outer
124             timer on return, elapsed time of a running code is taken into account.
125              
126             =item $t->run_and_catch($coderef)
127              
128             Run a code reference, as per C<$t-<gt>run()>, but also catching any
129             C<die()> calls within the code reference.
130              
131             Returns C<undef> if no C<die()> call was executed and C<$@> was unset, or the
132             value of C<$@> if it was set. (The timeout event doesn't count as a C<die()>.)
133              
134             =cut
135              
136              
137              
138 2012     2012 1 5399 my ($self, $sub, $and_catch) = @_;
139              
140 13     13 1 72 delete $self->{timed_out};
141              
142             my $id = $self->{id};
143 2025     2025   3285 my $secs = $self->{secs};
144             my $deadline = $self->{deadline};
145 2025         2753 my $alarm_tinkered_with = 0;
146             # dbg("timed: %s run", $id);
147 2025         2669  
148 2025         2678 # assertion
149 2025         2517 if (defined $secs && $secs < 0) {
150 2025         2352 die "Mail::SpamAssassin::Timeout: oops? neg value for 'secs': $secs";
151             }
152              
153             my $start_time = time;
154 2025 50 66     3987 if (defined $deadline) {
155 0         0 my $dt = $deadline - $start_time;
156             $secs = $dt if !defined $secs || $dt < $secs;
157             }
158 2025         4122  
159 2025 100       3350 # bug 4699: under heavy load, an alarm may fire while $@ will contain "",
160 2008         2740 # which isn't very useful. this flag works around it safely, since
161 2008 100 100     4672 # it will not require malloc() be called if it fires
162             my $timedout = 0;
163              
164             my($oldalarm, $handler);
165             if (defined $secs) {
166             # stop the timer, collect remaining time
167 2025         2484 $oldalarm = alarm(0); # 0 when disarmed, undef on error
168             $alarm_tinkered_with = 1;
169 2025         2478 if (!@expiration) {
170 2025 100       3196 # dbg("timed: %s no timer in evidence", $id);
171             # dbg("timed: %s actual timer was running, time left %.3f s",
172 2021         14241 # $id, $oldalarm) if $oldalarm;
173 2021         4360 } elsif (!defined $expiration[0]) {
174 2021 100       5256 # dbg("timed: %s timer not running according to evidence", $id);
    50          
175             # dbg("timed: %s actual timer was running, time left %.3f s",
176             # $id, $oldalarm) if $oldalarm;
177             } else {
178             my $oldalarm2 = $expiration[0] - $start_time;
179             # dbg("timed: %s stopping timer, time left %.3f s%s", $id, $oldalarm2,
180             # !$oldalarm ? '' : sprintf(", reported as %.3f s", $oldalarm));
181             $oldalarm = $oldalarm2 < 1 ? 1 : $oldalarm2;
182             }
183 1908         2481 $self->{end_time} = $start_time + $secs; # needed by reset()
184             $handler = sub { $timedout = 1; die "__alarm__ignore__($id)\n" };
185             }
186 1908 100       4373  
187             my($ret, $eval_stat);
188 2021         4237 unshift(@expiration, undef);
189 2021     12   8248 eval {
  12         18002630  
  12         851  
190             local $SIG{__DIE__}; # bug 4631
191              
192 2025         3269 if (!defined $secs) { # no timeout specified, just call the sub
193 2025         3350 $ret = &$sub;
194              
195 2025         6477 } elsif ($secs <= 0) {
196             $self->{timed_out} = 1;
197 2025 100 100     8589 &$handler;
    50          
    100          
198 4         8  
199             } elsif ($oldalarm && $oldalarm < $secs) { # run under an outer timer
200             # just restore outer timer, a timeout signal will be handled there
201 0         0 # dbg("timed: %s alarm(%.3f) - outer", $id, $oldalarm);
202 0         0 $expiration[0] = $start_time + $oldalarm;
203             alarm($oldalarm); $alarm_tinkered_with = 1;
204             $ret = &$sub;
205             # dbg("timed: %s post-sub(outer)", $id);
206              
207 1         29 } else { # run under a timer specified with this call
208 1         7 local $SIG{ALRM} = $handler; # ensure closed scope here
  1         14  
209 1         4 my $isecs = int($secs);
210             $isecs++ if $secs > int($isecs); # ceiling
211             # dbg("timed: %s alarm(%d)", $id, $isecs);
212             $expiration[0] = $start_time + $isecs;
213 2020         23926 alarm($isecs); $alarm_tinkered_with = 1;
214 2020         5057 $ret = &$sub;
215 2020 100       4519 # dbg("timed: %s post-sub", $id);
216             }
217 2020         3127  
218 2020         9525 # Unset the alarm() before we leave eval{ } scope, as that stack-pop
  2020         3832  
219 2020         5086 # operation can take a second or two under load. Note: previous versions
220             # restored $oldalarm here; however, that is NOT what we want to do, since
221             # it creates a new race condition, namely that an old alarm could then fire
222             # while the stack-pop was underway, thereby appearing to be *this* timeout
223             # timing out. In terms of how we might possibly have nested timeouts in
224             # SpamAssassin, this is an academic issue with little impact, but it's
225             # still worth avoiding anyway.
226             #
227             alarm(0) if $alarm_tinkered_with; # disarm
228              
229             1;
230             } or do {
231             $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
232 2005 100       7041633 # just in case we popped out for some other reason
233             alarm(0) if $alarm_tinkered_with; # disarm
234 2005         9876 };
235 2025 100       2853  
236 20 50       2005043 delete $self->{end_time}; # reset() is only applicable within a &$sub
  20         93  
237              
238 20 100       297 # catch timedout return:
239             # 0 0 $ret
240             # 0 1 undef
241 2025         3891 # 1 0 $eval_stat
242             # 1 1 undef
243             #
244             my $return = $and_catch ? $eval_stat : $ret;
245              
246             if (defined $eval_stat && $eval_stat =~ /__alarm__ignore__\Q($id)\E/) {
247             $self->{timed_out} = 1;
248             # dbg("timed: %s cought: %s", $id, $eval_stat);
249 2025 100       4117 } elsif ($timedout) {
250             # this happens occasionally; haven't figured out why. seems harmless
251 2025 100 100     6296 # dbg("timed: %s timeout with empty eval status", $id);
    50          
252 12         82 $self->{timed_out} = 1;
253             }
254              
255             shift(@expiration); # pop off the stack
256              
257 0         0 # covers all cases, including where $self->{timed_out} is flagged by reset()
258             undef $return if $self->{timed_out};
259              
260 2025         2809 my $remaining_time;
261             # restore previous timer if necessary
262             if ($oldalarm) { # an outer alarm was already active when we were called
263 2025 100       3680 $remaining_time = $start_time + $oldalarm - time;
264             if ($remaining_time > 0) { # still in the future
265 2025         2272 # restore the previously-active alarm,
266             # taking into account the elapsed time we spent here
267 2025 100       3320 my $iremaining_time = int($remaining_time);
268 1908         4674 $iremaining_time++ if $remaining_time > int($remaining_time); # ceiling
269 1908 100       3771 # dbg("timed: %s restoring outer alarm(%.3f)", $id, $iremaining_time);
270             alarm($iremaining_time); $alarm_tinkered_with = 1;
271             undef $remaining_time; # already taken care of
272 1905         2700 }
273 1905 50       3879 }
274             if (!$and_catch && defined $eval_stat &&
275 1905         8856 $eval_stat !~ /__alarm__ignore__\Q($id)\E/) {
  1905         3870  
276 1905         3028 # propagate "real" errors or outer timeouts
277             die "Timeout::_run: $eval_stat\n";
278             }
279 2025 100 100     7870 if (defined $remaining_time) {
      100        
280             # dbg("timed: %s outer timer expired %.3f s ago", $id, -$remaining_time);
281             # mercifully grant two additional seconds
282 3         60 alarm(2); $alarm_tinkered_with = 1;
283             }
284 2022 100       3421 return $return;
285             }
286              
287 2         27 ###########################################################################
  2         9  
288              
289 2022         12873 =item $t->timed_out()
290              
291             Returns C<1> if the most recent code executed in C<run()> timed out, or
292             C<undef> if it did not.
293              
294             =cut
295              
296             my ($self) = @_;
297             return $self->{timed_out};
298             }
299              
300             ###########################################################################
301              
302 1832     1832 1 3663 =item $t->reset()
303 1832         11665  
304             If called within a C<run()> code reference, causes the current alarm timer
305             to be restored to its original setting (useful after our alarm setting was
306             clobbered by some underlying module).
307              
308             =back
309              
310             =cut
311              
312             my ($self) = @_;
313              
314             my $id = $self->{id};
315             # dbg("timed: %s reset", $id);
316             return if !defined $self->{end_time};
317              
318             my $secs = $self->{end_time} - time;
319 3     3 1 6013799 if ($secs > 0) {
320             my $isecs = int($secs);
321 3         19 $isecs++ if $secs > int($isecs); # ceiling
322             # dbg("timed: %s reset: alarm(%.3f)", $self->{id}, $isecs);
323 3 50       21 alarm($isecs);
324             } else {
325 3         26 $self->{timed_out} = 1;
326 3 100       24 # dbg("timed: %s reset, timer expired %.3f s ago", $id, -$secs);
327 2         7 alarm(2); # mercifully grant two additional seconds
328 2 50       9 }
329             }
330 2         28  
331             ###########################################################################
332 1         9  
333             1;