Search code examples
pythonpython-2.7python-decorators

Python timeout decorator


I'm using the code solution mentioned here :

import signal

class TimeoutError(Exception):
    def __init__(self, value = "Timed Out"):
        self.value = value
    def __str__(self):
        return repr(self.value)

def timeout(seconds_before_timeout):
    def decorate(f):
        def handler(signum, frame):
            raise TimeoutError()
        def new_f(*args, **kwargs):
            old = signal.signal(signal.SIGALRM, handler)
            signal.alarm(seconds_before_timeout)
            try:
                result = f(*args, **kwargs)
            finally:
                # reinstall the old signal handler
                signal.signal(signal.SIGALRM, old)
                # cancel the alarm
                # this line should be inside the "finally" block (per Sam Kortchmar)
                signal.alarm(0)
            return result
        new_f.func_name = f.func_name
        return new_f
    return decorate

Try it out:

import time

@timeout(5)
def mytest():
    print "Start"
    for i in range(1,10):
        time.sleep(1)
        print "%d seconds have passed" % i

if __name__ == '__main__':
    mytest()

Results:

Start
1 seconds have passed
2 seconds have passed
3 seconds have passed
4 seconds have passed
Traceback (most recent call last):
  File "timeout_ex.py", line 47, in <module>
    function_times_out()
  File "timeout_ex.py", line 17, in new_f
    result = f(*args, **kwargs)
  File "timeout_ex.py", line 42, in function_times_out
    time.sleep(1)
  File "timeout_ex.py", line 12, in handler
    raise TimeoutError()
__main__.TimeoutError: 'Timed Out'

I'm new to decorators, and don't understand why this solution doesn't work if I want to write something like the following:

    @timeout(10)
    def main_func():
        nested_func()
        while True:
            continue
    
    @timeout(5)
    def nested_func():
       print "finished doing nothing"

=> Result of this will be no timeout at all. We will be stuck on endless loop.
However if I remove @timeout annotation from nested_func I get a timeout error.
For some reason we can't use decorator on function and on a nested function in the same time, any idea why and how can I correct it to work, assume that containing function timeout always must be bigger than the nested timeout.


Solution

  • This is a limitation of the signal module's timing functions, which the decorator you linked uses. Here's the relevant piece of the documentation (with emphasis added by me):

    signal.alarm(time)

    If time is non-zero, this function requests that a SIGALRM signal be sent to the process in time seconds. Any previously scheduled alarm is canceled (only one alarm can be scheduled at any time). The returned value is then the number of seconds before any previously set alarm was to have been delivered. If time is zero, no alarm is scheduled, and any scheduled alarm is canceled. If the return value is zero, no alarm is currently scheduled. (See the Unix man page alarm(2).) Availability: Unix.

    So, what you're seeing is that when your nested_func is called, it's timer cancels the outer function's timer.

    You can update the decorator to pay attention to the return value of the alarm call (which will be the time before the previous alarm (if any) was due). It's a bit complicated to get the details right, since the inner timer needs to track how long its function ran for, so it can modify the time remaining on the previous timer. Here's an untested version of the decorator that I think gets it mostly right (but I'm not entirely sure it works correctly for all exception cases):

    import time
    import signal
    
    class TimeoutError(Exception):
        def __init__(self, value = "Timed Out"):
            self.value = value
        def __str__(self):
            return repr(self.value)
    
    def timeout(seconds_before_timeout):
        def decorate(f):
            def handler(signum, frame):
                raise TimeoutError()
            def new_f(*args, **kwargs):
                old = signal.signal(signal.SIGALRM, handler)
                old_time_left = signal.alarm(seconds_before_timeout)
                if 0 < old_time_left < second_before_timeout: # never lengthen existing timer
                    signal.alarm(old_time_left)
                start_time = time.time()
                try:
                    result = f(*args, **kwargs)
                finally:
                    if old_time_left > 0: # deduct f's run time from the saved timer
                        old_time_left -= time.time() - start_time
                    signal.signal(signal.SIGALRM, old)
                    signal.alarm(old_time_left)
                return result
            new_f.func_name = f.func_name
            return new_f
        return decorate