Timeout on a function call

后端 未结 18 1473
挽巷
挽巷 2020-11-21 04:53

I\'m calling a function in Python which I know may stall and force me to restart the script.

How do I call the function or what do I wrap it in so that if it takes

相关标签:
18条回答
  • 2020-11-21 04:59

    How do I call the function or what do I wrap it in so that if it takes longer than 5 seconds the script cancels it?

    I posted a gist that solves this question/problem with a decorator and a threading.Timer. Here it is with a breakdown.

    Imports and setups for compatibility

    It was tested with Python 2 and 3. It should also work under Unix/Linux and Windows.

    First the imports. These attempt to keep the code consistent regardless of the Python version:

    from __future__ import print_function
    import sys
    import threading
    from time import sleep
    try:
        import thread
    except ImportError:
        import _thread as thread
    

    Use version independent code:

    try:
        range, _print = xrange, print
        def print(*args, **kwargs): 
            flush = kwargs.pop('flush', False)
            _print(*args, **kwargs)
            if flush:
                kwargs.get('file', sys.stdout).flush()            
    except NameError:
        pass
    

    Now we have imported our functionality from the standard library.

    exit_after decorator

    Next we need a function to terminate the main() from the child thread:

    def quit_function(fn_name):
        # print to stderr, unbuffered in Python 2.
        print('{0} took too long'.format(fn_name), file=sys.stderr)
        sys.stderr.flush() # Python 3 stderr is likely buffered.
        thread.interrupt_main() # raises KeyboardInterrupt
    

    And here is the decorator itself:

    def exit_after(s):
        '''
        use as decorator to exit process if 
        function takes longer than s seconds
        '''
        def outer(fn):
            def inner(*args, **kwargs):
                timer = threading.Timer(s, quit_function, args=[fn.__name__])
                timer.start()
                try:
                    result = fn(*args, **kwargs)
                finally:
                    timer.cancel()
                return result
            return inner
        return outer
    

    Usage

    And here's the usage that directly answers your question about exiting after 5 seconds!:

    @exit_after(5)
    def countdown(n):
        print('countdown started', flush=True)
        for i in range(n, -1, -1):
            print(i, end=', ', flush=True)
            sleep(1)
        print('countdown finished')
    

    Demo:

    >>> countdown(3)
    countdown started
    3, 2, 1, 0, countdown finished
    >>> countdown(10)
    countdown started
    10, 9, 8, 7, 6, countdown took too long
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 11, in inner
      File "<stdin>", line 6, in countdown
    KeyboardInterrupt
    

    The second function call will not finish, instead the process should exit with a traceback!

    KeyboardInterrupt does not always stop a sleeping thread

    Note that sleep will not always be interrupted by a keyboard interrupt, on Python 2 on Windows, e.g.:

    @exit_after(1)
    def sleep10():
        sleep(10)
        print('slept 10 seconds')
    
    >>> sleep10()
    sleep10 took too long         # Note that it hangs here about 9 more seconds
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 11, in inner
      File "<stdin>", line 3, in sleep10
    KeyboardInterrupt
    

    nor is it likely to interrupt code running in extensions unless it explicitly checks for PyErr_CheckSignals(), see Cython, Python and KeyboardInterrupt ignored

    I would avoid sleeping a thread more than a second, in any case - that's an eon in processor time.

    How do I call the function or what do I wrap it in so that if it takes longer than 5 seconds the script cancels it and does something else?

    To catch it and do something else, you can catch the KeyboardInterrupt.

    >>> try:
    ...     countdown(10)
    ... except KeyboardInterrupt:
    ...     print('do something else')
    ... 
    countdown started
    10, 9, 8, 7, 6, countdown took too long
    do something else
    
    0 讨论(0)
  • 2020-11-21 04:59

    There are a lot of suggestions, but none using concurrent.futures, which I think is the most legible way to handle this.

    from concurrent.futures import ProcessPoolExecutor
    
    # Warning: this does not terminate function if timeout
    def timeout_five(fnc, *args, **kwargs):
        with ProcessPoolExecutor() as p:
            f = p.submit(fnc, *args, **kwargs)
            return f.result(timeout=5)
    

    Super simple to read and maintain.

    We make a pool, submit a single process and then wait up to 5 seconds before raising a TimeoutError that you could catch and handle however you needed.

    Native to python 3.2+ and backported to 2.7 (pip install futures).

    Switching between threads and processes is as simple as replacing ProcessPoolExecutor with ThreadPoolExecutor.

    If you want to terminate the Process on timeout I would suggest looking into Pebble.

    0 讨论(0)
  • 2020-11-21 04:59

    I am the author of wrapt_timeout_decorator

    Most of the solutions presented here work wunderfully under Linux on the first glance - because we have fork() and signals() - but on windows the things look a bit different. And when it comes to subthreads on Linux, You cant use Signals anymore.

    In order to spawn a process under Windows, it needs to be picklable - and many decorated functions or Class methods are not.

    So You need to use a better pickler like dill and multiprocess (not pickle and multiprocessing) - thats why You cant use ProcessPoolExecutor (or only with limited functionality).

    For the timeout itself - You need to define what timeout means - because on Windows it will take considerable (and not determinable) time to spawn the process. This can be tricky on short timeouts. Lets assume, spawning the process takes about 0.5 seconds (easily !!!). If You give a timeout of 0.2 seconds what should happen ? Should the function time out after 0.5 + 0.2 seconds (so let the method run for 0.2 seconds)? Or should the called process time out after 0.2 seconds (in that case, the decorated function will ALWAYS timeout, because in that time it is not even spawned) ?

    Also nested decorators can be nasty and You cant use Signals in a subthread. If You want to create a truly universal, cross-platform decorator, all this needs to be taken into consideration (and tested).

    Other issues are passing exceptions back to the caller, as well as logging issues (if used in the decorated function - logging to files in another process is NOT supported)

    I tried to cover all edge cases, You might look into the package wrapt_timeout_decorator, or at least test Your own solutions inspired by the unittests used there.

    @Alexis Eggermont - unfortunately I dont have enough points to comment - maybe someone else can notify You - I think I solved Your import issue.

    0 讨论(0)
  • 2020-11-21 04:59

    Here is a slight improvement to the given thread-based solution.

    The code below supports exceptions:

    def runFunctionCatchExceptions(func, *args, **kwargs):
        try:
            result = func(*args, **kwargs)
        except Exception, message:
            return ["exception", message]
    
        return ["RESULT", result]
    
    
    def runFunctionWithTimeout(func, args=(), kwargs={}, timeout_duration=10, default=None):
        import threading
        class InterruptableThread(threading.Thread):
            def __init__(self):
                threading.Thread.__init__(self)
                self.result = default
            def run(self):
                self.result = runFunctionCatchExceptions(func, *args, **kwargs)
        it = InterruptableThread()
        it.start()
        it.join(timeout_duration)
        if it.isAlive():
            return default
    
        if it.result[0] == "exception":
            raise it.result[1]
    
        return it.result[1]
    

    Invoking it with a 5 second timeout:

    result = timeout(remote_calculate, (myarg,), timeout_duration=5)
    
    0 讨论(0)
  • 2020-11-21 05:02

    I had a need for nestable timed interrupts (which SIGALARM can't do) that won't get blocked by time.sleep (which the thread-based approach can't do). I ended up copying and lightly modifying code from here: http://code.activestate.com/recipes/577600-queue-for-managing-multiple-sigalrm-alarms-concurr/

    The code itself:

    #!/usr/bin/python
    
    # lightly modified version of http://code.activestate.com/recipes/577600-queue-for-managing-multiple-sigalrm-alarms-concurr/
    
    
    """alarm.py: Permits multiple SIGALRM events to be queued.
    
    Uses a `heapq` to store the objects to be called when an alarm signal is
    raised, so that the next alarm is always at the top of the heap.
    """
    
    import heapq
    import signal
    from time import time
    
    __version__ = '$Revision: 2539 $'.split()[1]
    
    alarmlist = []
    
    __new_alarm = lambda t, f, a, k: (t + time(), f, a, k)
    __next_alarm = lambda: int(round(alarmlist[0][0] - time())) if alarmlist else None
    __set_alarm = lambda: signal.alarm(max(__next_alarm(), 1))
    
    
    class TimeoutError(Exception):
        def __init__(self, message, id_=None):
            self.message = message
            self.id_ = id_
    
    
    class Timeout:
        ''' id_ allows for nested timeouts. '''
        def __init__(self, id_=None, seconds=1, error_message='Timeout'):
            self.seconds = seconds
            self.error_message = error_message
            self.id_ = id_
        def handle_timeout(self):
            raise TimeoutError(self.error_message, self.id_)
        def __enter__(self):
            self.this_alarm = alarm(self.seconds, self.handle_timeout)
        def __exit__(self, type, value, traceback):
            try:
                cancel(self.this_alarm) 
            except ValueError:
                pass
    
    
    def __clear_alarm():
        """Clear an existing alarm.
    
        If the alarm signal was set to a callable other than our own, queue the
        previous alarm settings.
        """
        oldsec = signal.alarm(0)
        oldfunc = signal.signal(signal.SIGALRM, __alarm_handler)
        if oldsec > 0 and oldfunc != __alarm_handler:
            heapq.heappush(alarmlist, (__new_alarm(oldsec, oldfunc, [], {})))
    
    
    def __alarm_handler(*zargs):
        """Handle an alarm by calling any due heap entries and resetting the alarm.
    
        Note that multiple heap entries might get called, especially if calling an
        entry takes a lot of time.
        """
        try:
            nextt = __next_alarm()
            while nextt is not None and nextt <= 0:
                (tm, func, args, keys) = heapq.heappop(alarmlist)
                func(*args, **keys)
                nextt = __next_alarm()
        finally:
            if alarmlist: __set_alarm()
    
    
    def alarm(sec, func, *args, **keys):
        """Set an alarm.
    
        When the alarm is raised in `sec` seconds, the handler will call `func`,
        passing `args` and `keys`. Return the heap entry (which is just a big
        tuple), so that it can be cancelled by calling `cancel()`.
        """
        __clear_alarm()
        try:
            newalarm = __new_alarm(sec, func, args, keys)
            heapq.heappush(alarmlist, newalarm)
            return newalarm
        finally:
            __set_alarm()
    
    
    def cancel(alarm):
        """Cancel an alarm by passing the heap entry returned by `alarm()`.
    
        It is an error to try to cancel an alarm which has already occurred.
        """
        __clear_alarm()
        try:
            alarmlist.remove(alarm)
            heapq.heapify(alarmlist)
        finally:
            if alarmlist: __set_alarm()
    

    and a usage example:

    import alarm
    from time import sleep
    
    try:
        with alarm.Timeout(id_='a', seconds=5):
            try:
                with alarm.Timeout(id_='b', seconds=2):
                    sleep(3)
            except alarm.TimeoutError as e:
                print 'raised', e.id_
            sleep(30)
    except alarm.TimeoutError as e:
        print 'raised', e.id_
    else:
        print 'nope.'
    
    0 讨论(0)
  • 2020-11-21 05:03

    You can use multiprocessing.Process to do exactly that.

    Code

    import multiprocessing
    import time
    
    # bar
    def bar():
        for i in range(100):
            print "Tick"
            time.sleep(1)
    
    if __name__ == '__main__':
        # Start bar as a process
        p = multiprocessing.Process(target=bar)
        p.start()
    
        # Wait for 10 seconds or until process finishes
        p.join(10)
    
        # If thread is still active
        if p.is_alive():
            print "running... let's kill it..."
    
            # Terminate - may not work if process is stuck for good
            p.terminate()
            # OR Kill - will work for sure, no chance for process to finish nicely however
            # p.kill()
    
            p.join()
    
    0 讨论(0)
提交回复
热议问题