Recipe 16.21 Timing Out an Operation

16.21.1 Problem

You want to make sure an operation doesn't take more than a certain amount of time. For instance, you're running filesystem backups and want to abort if it takes longer than an hour. Or, you want to give the user a limited amount of time to respond to a query.

16.21.2 Solution

To interrupt a long-running operation, set a SIGALRM handler to call die, in effect transforming the signal into an exception. Set an alarm with alarm, then eval your code:

eval { 
    local $SIG{ALRM} = sub { die "alarm clock restart" };
    alarm 10;                   # schedule alarm in 10 seconds 
    eval { 
          # long-running operation goes here
    alarm 0;                    # cancel the alarm
alarm 0;                        # race condition protection
die if $@ && $@ !~ /alarm clock restart/; # reraise

16.21.3 Discussion

The alarm function takes one argument: the integer number of seconds before the kernel sends your process a SIGALRM, that is, an alarm signal. It may be delivered after that time in busy time-sharing systems. The default action for SIGALRM is to terminate your program, so you should install your own signal handler.

Because this example should work no matter what operation is being timed out, we take some special precautions in case your long-running operation contains a slow syscall. Slow syscalls are those that don't return immediately, but await some external event, such as for I/O to happen or some sort of timer to go off. These external events include read (including readline, the <FH> operator), write, and open on certain devices, fifos, and sockets, as well as accept, connect, send, recv, flock, wait, waitpid, and of course, sleep. If the alarm hits while you're in a slow syscall and you simply catch the signal and return, you'll go right back into that syscall. That's because Perl automatically restarts syscalls where it's able to. The only way out of them is to raise an exception through die and then let eval catch it. (This works because the exception winds up calling the C library's longjmp(3) function, which is what really gets you out of the restarting syscall.)

The nested exception trap is because you cannot be sure that arbitrary code in your long-running operation isn't going to raise some exception of its own. If it did, then that would pop you out of the inner eval with the alarm still pending. You need to make sure to clear the alarm anyway. The second alarm 0 is in case the signal comes in after running the long-running operation, but before getting to the first alarm 0. If you don't do that, you would risk a tiny race conditionbut size doesn't matter in race conditions; they either exist or they don't. And we prefer that they don't.

You cannot (usefully) give the alarm function a fractional number of seconds; if you try, it will be truncated to an integer. For precise timers, see Recipe 3.9.

16.21.4 See Also

The "Signals" sections in Chapter 16 of Programming Perl and in perlipc(1); the section on "Handling Race Conditions" in Chapter 23 of Programming Perl; the alarm function in Chapter 29 of Programming Perl and in perlfunc(1); Recipe 3.9