10

I'm using the code solution mentioned here.
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.

JavaSa
  • 5,813
  • 16
  • 71
  • 121

3 Answers3

8

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
Community
  • 1
  • 1
Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • Which exception cases you are not sure about? – JavaSa Feb 19 '16 at 19:13
  • I guess it's not so much the exception cases, as the `finally` block cleans things up pretty well, but the situations where an previous alarm has been set are probably not handled the best they could be. I'm also not been able to test the code, since `signal.alarm` isn't available on my OS. – Blckknght Feb 20 '16 at 01:57
  • I think we need to add also a check that old_time_left -= time.time() - start_time >0, it can be negative I think – JavaSa Feb 21 '16 at 13:01
  • @JavaSa I agree about old_time_left that could be negative and then not sure that `signal.alarm(old_time_left)` will be happy. – Patrick Mevzek Jul 23 '19 at 23:38
2

as Blckknght pointed out, You can't use signals for nested decorators - but you can use multiprocessing to achieve that.

You might use this decorator, it supports nested decorators : https://github.com/bitranox/wrapt_timeout_decorator

and as ABADGER1999 points out in his blog https://anonbadger.wordpress.com/2018/12/15/python-signal-handlers-and-exceptions/ using signals and the TimeoutException is probably not the best idea - because it can be caught in the decorated function.

Of course you can use your own Exception, derived from the Base Exception Class, but the code might still not work as expected - see the next example - you may try it out in jupyter: https://mybinder.org/v2/gh/bitranox/wrapt_timeout_decorator/master?filepath=jupyter_test_wrapt_timeout_decorator.ipynb

import time
from wrapt_timeout_decorator import *

# caveats when using signals - the TimeoutError raised by the signal may be caught
# inside the decorated function.
# So You might use Your own Exception, derived from the base Exception Class.
# In Python-3.7.1 stdlib there are over 300 pieces of code that will catch your timeout
# if you were to base an exception on Exception. If you base your exception on BaseException,
# there are still 231 places that can potentially catch your exception.
# You should use use_signals=False if You want to make sure that the timeout is handled correctly !
# therefore the default value for use_signals = False on this decorator !

@timeout(5, use_signals=True)
def mytest(message):
    try:
        print(message)
        for i in range(1,10):
            time.sleep(1)
            print('{} seconds have passed - lets assume we read a big file here'.format(i))
    # TimeoutError is a Subclass of OSError - therefore it is caught here !
    except OSError:
        for i in range(1,10):
            time.sleep(1)
            print('Whats going on here ? - Ooops the Timeout Exception is catched by the OSError ! {}'.format(i))
    except Exception:
        # even worse !
        pass
    except:
        # the worst - and exists more then 300x in actual Python 3.7 stdlib Code !
        # so You never really can rely that You catch the TimeoutError when using Signals !
        pass


if __name__ == '__main__':
    try:
        mytest('starting')
        print('no Timeout Occured')
    except TimeoutError():
        # this will never be printed because the decorated function catches implicitly the TimeoutError !
        print('Timeout Occured')
Jeroen Steenbeeke
  • 3,884
  • 5
  • 17
  • 26
bitranox
  • 1,664
  • 13
  • 21
1

There's a better version of timeout decorator that's currently on Python's PyPI library. It supports both UNIX and non-UNIX based operating system. The part where SIGNALS are mentioned - that specifically for UNIX.

Assuming you aren't using UNIX. Below is a code snippet from the decorator that shows you a list of parameters that you can use as required.

def timeout(seconds=None, use_signals=True, timeout_exception=TimeoutError, exception_message=None)

For implementation on NON-UNIX base operating system. This is what I would do:

import time
import timeout_decorator

@timeout_decorator.timeout(10, use_signals=False)
def main_func():
    nested_func()
    while True:
        continue

@timeout_decorator.timeout(5, use_signals=False)
def nested_func():
    print "finished doing nothing"

If you notice, I'm doing use_signals=False. That's all, you should be good to go.

Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
AteamSF
  • 31
  • 4
  • 1
    error on windows 10: File "", line 1, in File "C:\Program Files\Python37\lib\multiprocessing\spawn.py", line 99, in spawn_main new_handle = reduction.steal_handle(parent_pid, pipe_handle) File "C:\Program Files\Python37\lib\multiprocessing\reduction.py", line 87, in steal_handle _winapi.DUPLICATE_SAME_ACCESS | _winapi.DUPLICATE_CLOSE_SOURCE) PermissionError: [WinError 5] Access is denied – Andy Yuan Dec 20 '21 at 06:22
  • This is working on my machine (linux). Though, there are big setbacks: this module uses multiprocessing under the hood if you set `use_signals=False`. In my case, I was trying to use it with multithreading and a queue of arguments stored in a shared list: timeout_decorator won't let you "pop" from this shared list (it will create a copy of the list for each pseudo "thread"). Saddly, this is still the case when you store your list inside a manager... – tgrandje Dec 14 '22 at 13:11