http://www.perlmonks.org?node_id=628706

tlm has asked for the wisdom of the Perl Monks concerning the following question:

The standard technique for timing-out long-running procedures in Perl is something like this:

{ local $SIG{ ALRM } = sub { die 'timeout' }; alarm( MAX_SECONDS ); eval { molasses_in_january(); alarm( 0 ); }; if ( $@ ) { die if $@ !~ /timeout/; handle_timeout(); } }

(The Perl Cookbook, q.v., gives a slightly more elaborate technique featuring a second eval inside the first one, but the basic idea is the same.)

One serious problem with this technique is that it requires that the code running in the dynamic scope inside the eval does not itself use an eval, or, if it does, that it faithfully propagates our timeout error. These are very shaky assumptions...

In fact, I find myself now using a library written (badly) by someone else, and I'm finding that I can't safely timeout a certain long-running procedure because somewhere deep into the called code there's an eval that looks something like this

$cowbell = eval { more_cowbell() }; # eval errors blithely ignored ... # in some other code, far, far away die 'i got a fever' unless $cowbell;

This means that the code after my eval cannot distinguish between a timeout and some other error that may have happened...

And to make matters worse, due to the widespread uses of careless eval's like this one, I don't even know exactly which one of all of them is ultimately responsible for trapping and ignoring my ALRM signal. (My rendition of the problematic code radically simplifies the problem; in reality the relationship between the variables initialized as results of rogue eval's and the test that ultimately triggers the die statement is far more baroque and unclear than what I'm showing.)

I suppose that I could time the operation and use the measured elapsed time as a heuristic to decide whether the uninformative error message corresponds to a "real" error or to a disguised time-out error. But this is a very crude workaround.

Is there something better?

Also, any trick that may help spot which of all the evals in the alien code is responsible for this particular mess would be appreciated.

TIA!

the lowliest monk

Replies are listed 'Best First'.
Re: On timing out with ALRM
by samtregar (Abbot) on Jul 25, 2007 at 17:00 UTC
    This is quite similar to the problem I solved with DBIx::Timeout. In that case I needed to timeout long DB queries but I couldn't rely on signals to do it safely. My solution was to fork off a process that waits until the timeout occurs and then kills the DB connection. In your case you'd probably have to fork the long-running code and then end it with kill(). It's not what you'd call a light-weight solution but it should work...

    Of course, an alternative would be to fix the code! Evals that throws away errors are really worth the time to fix. I've been bitten by that beast more than once while working on inherited code and it's amazing how much work they can cause when error messages for relatively simple problems just aren't getting to the error logs!

    -sam

Re: On timing out with ALRM
by xdg (Monsignor) on Jul 25, 2007 at 16:20 UTC

    It's not very elegant, but what about adding a lexical flag that gets set in your alarm handler?

    { my $timed_out = 0; local $SIG{ ALRM } = sub { $timed_out++; die 'timeout' }; alarm( MAX_SECONDS ); eval { molasses_in_january(); alarm( 0 ); }; if ( $@ ) { die $@ if ! $timed_out; handle_timeout(); } }

    -xdg

    Code written by xdg and posted on PerlMonks is public domain. It is provided as is with no warranties, express or implied, of any kind. Posted code may not have been tested. Use of posted code is at your own risk.

Re: On timing out with ALRM
by ikegami (Patriarch) on Jul 25, 2007 at 16:23 UTC

    It's impossible to impose a timeout on code which is not aware of the timeout because it would require to ability to end that code forcefully and it's impossible to safely end code forcefully.

    If you want to go down that line, your best bet it to end the entire process. If you just want to timeout molasses_in_january, then molasses_in_january would be executed in a different process than the one imposing the timeout.