You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spamassassin.apache.org by mm...@apache.org on 2009/11/21 02:13:58 UTC

svn commit: r882815 - /spamassassin/trunk/lib/Mail/SpamAssassin/Timeout.pm

Author: mmartinec
Date: Sat Nov 21 01:13:57 2009
New Revision: 882815

URL: http://svn.apache.org/viewvc?rev=882815&view=rev
Log:
M::S::Timeout - reworked the module to deal with nested
timers as one would expect: an inner timer shouldn't be able
to extend an outer timer's limit; account for time elapsed
in the submitted subroutine when restarting an outer timer

Modified:
    spamassassin/trunk/lib/Mail/SpamAssassin/Timeout.pm

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Timeout.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Timeout.pm?rev=882815&r1=882814&r2=882815&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Timeout.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Timeout.pm Sat Nov 21 01:13:57 2009
@@ -58,7 +58,7 @@
 use bytes;
 use re 'taint';
 
-use Time::HiRes qw(time alarm);
+use Time::HiRes qw(time);
 
 use vars qw{
   @ISA
@@ -89,10 +89,14 @@
 
 =cut
 
+use vars qw($id_gen);
+BEGIN { $id_gen = 0 }  # unique generator of IDs for timer objects
+
 sub new {
   my ($class, $opts) = @_;
   $class = ref($class) || $class;
   my %selfval = $opts ? %{$opts} : ();
+  $selfval{id} = ++$id_gen;
   my $self = \%selfval;
 
   bless ($self, $class);
@@ -112,7 +116,11 @@
 If the timer times out, C<$t-<gt>timed_out()> will return C<1>.
 
 Time elapsed is not cumulative; multiple runs of C<run> will restart the
-timeout from scratch.
+timeout from scratch. On the other hand, nested timers do observe outer
+timeouts if they are shorter, resignalling a timeout to the level which
+established them, i.e. code running under an inner timer can not exceed
+the time limit established by an outer timer. When restarting an outer
+timer the elapsed time of a running code is taken into account.
 
 =item $t->run_and_catch($coderef)
 
@@ -136,9 +144,13 @@
   my $secs = $self->{secs};
   my $deadline = $self->{deadline};
 
+  # assertion
+  if (defined $secs && $secs < 0) {
+    die "Mail::SpamAssassin::Timeout: oops? neg value for 'secs': $secs";
+  }
+
   if (defined $deadline) {
     my $dt = $deadline - time;
-    $dt = 1  if $dt < 1;  # give some slack
     $secs = $dt  if !defined $secs || $dt < $secs;
   }
 
@@ -146,28 +158,36 @@
     return &$sub;
   }
 
-  # assertion
-  if ($secs < 0) {
-    die "Mail::SpamAssassin::Timeout: oops? neg value for 'secs': $secs";
-  }
-
-  my $oldalarm = 0;
+  my $id = $self->{id};
   my $ret;
+  my $oldalarm = alarm(0);  # remaining time, 0 when disarmed, undef on error
+  my $start_time = time;
 
   # bug 4699: under heavy load, an alarm may fire while $@ will contain "",
-  # which isn't very useful.  this counter works around it safely, since
+  # which isn't very useful.  this flag works around it safely, since
   # it will not require malloc() be called if it fires
   my $timedout = 0;
 
   my $eval_stat;
   eval {
-    # note use of local to ensure closed scope here
-    local $SIG{ALRM} = sub { $timedout++; die "__alarm__ignore__\n" };
-    local $SIG{__DIE__};   # bug 4631
+    if ($secs <= 0) {
+      die "__alarm__ignore__($id) (pre-expired)\n";
 
-    $oldalarm = alarm($secs);
+    } elsif ($oldalarm && $oldalarm < $secs) {
+      # just restore outer timer, a timeout signal will be handled there
+      alarm($oldalarm);
+      $ret = &$sub;
 
-    $ret = &$sub;
+    } else {
+      # note use of local to ensure closed scope here
+      local $SIG{ALRM} = sub { $timedout = 1;  die "__alarm__ignore__($id)\n" };
+      local $SIG{__DIE__};   # bug 4631
+
+      my $isecs = int($secs);
+      $isecs++  if $secs > int($isecs);  # ceiling
+      alarm($isecs);
+      $ret = &$sub;
+    }
 
     # Unset the alarm() before we leave eval{ } scope, as that stack-pop
     # operation can take a second or two under load. Note: previous versions
@@ -177,36 +197,55 @@
     # timing out. In terms of how we might possibly have nested timeouts in
     # SpamAssassin, this is an academic issue with little impact, but it's
     # still worth avoiding anyway.
+    #
+    alarm(0);  # disarm
 
-    alarm 0;
     1;
   } or do {
     $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+    alarm(0);  # in case we popped out for some other reason
   };
 
-  if (defined $oldalarm) {
-    # now, we could have died from a SIGALRM == timed out.  if so,
-    # restore the previously-active one, or zero all timeouts if none
-    # were previously active.
-    alarm $oldalarm;
-  }
+  # catch timedout  return:
+  #    0    0       $ret
+  #    0    1       undef
+  #    1    0       $eval_stat
+  #    1    1       undef
+  #
+  my $return = $and_catch ? $eval_stat : $ret;
 
   if (defined $eval_stat) {
-    if ($eval_stat =~ /__alarm__ignore__/) {
-      $self->{timed_out} = 1;
-    } elsif ($and_catch) {
-      return $eval_stat;
-    } else {
-      die "Timeout::_run: $eval_stat\n";  # propagate any "real" errors
+    if ($eval_stat =~ /__alarm__ignore__\Q($id)\E/) {
+      undef $return;  $self->{timed_out} = 1;
     }
   } elsif ($timedout) {
     # this happens occasionally; haven't figured out why.  seems
     # harmless in effect, though, so just issue a warning and carry on...
     warn "timeout with empty eval status\n";
-    $self->{timed_out} = 1;
+    undef $return;  $self->{timed_out} = 1;
   }
 
-  return $and_catch ? undef : $ret;
+  my $remaining_time;
+  if ($oldalarm) {
+    $remaining_time = $start_time + $oldalarm - time;
+    if ($remaining_time > 0) {  # still in the future
+      # restore the previously-active alarm,
+      # taking into account the elapsed time we spent here
+      my $iremaining_time = int($remaining_time);
+      $iremaining_time++  if $remaining_time > int($remaining_time); # ceiling
+      alarm($iremaining_time);
+      undef $remaining_time;  # already taken care of
+    }
+  }
+  if (!$and_catch && defined $eval_stat &&
+      $eval_stat !~ /__alarm__ignore__\Q($id)\E/) {
+    # propagate "real" errors or outer timeouts
+    die "Timeout::_run: $eval_stat\n";
+  }
+  if (defined $remaining_time) {
+    kill('ALRM',0);  # previous timer expired meanwhile, re-signal right away
+  }
+  return $return;
 }
 
 ###########################################################################
@@ -240,10 +279,18 @@
 
   if (defined $deadline) {
     my $dt = $deadline - time;
-    $dt = 1  if $dt < 1;  # give some slack
     $secs = $dt  if !defined $secs || $dt < $secs;
   }
-  alarm($secs)  if defined $secs;
+
+  if (!defined $secs) {
+    # not timed
+  } elsif ($secs > 0) {
+    my $isecs = int($secs);
+    $isecs++  if $secs > int($isecs);  # ceiling
+    alarm($isecs);
+  } else {
+    kill('ALRM',0);  # previous timer expired meanwhile, re-signal right away
+  }
 }
 
 ###########################################################################