Timeout on a function call

后端 未结 18 1301
挽巷
挽巷 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 05:08

    Here is a POSIX version that combines many of the previous answers to deliver following features:

    1. Subprocesses blocking the execution.
    2. Usage of the timeout function on class member functions.
    3. Strict requirement on time-to-terminate.

    Here is the code and some test cases:

    import threading
    import signal
    import os
    import time
    
    class TerminateExecution(Exception):
        """
        Exception to indicate that execution has exceeded the preset running time.
        """
    
    
    def quit_function(pid):
        # Killing all subprocesses
        os.setpgrp()
        os.killpg(0, signal.SIGTERM)
    
        # Killing the main thread
        os.kill(pid, signal.SIGTERM)
    
    
    def handle_term(signum, frame):
        raise TerminateExecution()
    
    
    def invoke_with_timeout(timeout, fn, *args, **kwargs):
        # Setting a sigterm handler and initiating a timer
        old_handler = signal.signal(signal.SIGTERM, handle_term)
        timer = threading.Timer(timeout, quit_function, args=[os.getpid()])
        terminate = False
    
        # Executing the function
        timer.start()
        try:
            result = fn(*args, **kwargs)
        except TerminateExecution:
            terminate = True
        finally:
            # Restoring original handler and cancel timer
            signal.signal(signal.SIGTERM, old_handler)
            timer.cancel()
    
        if terminate:
            raise BaseException("xxx")
    
        return result
    
    ### Test cases
    def countdown(n):
        print('countdown started', flush=True)
        for i in range(n, -1, -1):
            print(i, end=', ', flush=True)
            time.sleep(1)
        print('countdown finished')
        return 1337
    
    
    def really_long_function():
        time.sleep(10)
    
    
    def really_long_function2():
        os.system("sleep 787")
    
    
    # Checking that we can run a function as expected.
    assert invoke_with_timeout(3, countdown, 1) == 1337
    
    # Testing various scenarios
    t1 = time.time()
    try:
        print(invoke_with_timeout(1, countdown, 3))
        assert(False)
    except BaseException:
        assert(time.time() - t1 < 1.1)
        print("All good", time.time() - t1)
    
    t1 = time.time()
    try:
        print(invoke_with_timeout(1, really_long_function2))
        assert(False)
    except BaseException:
        assert(time.time() - t1 < 1.1)
        print("All good", time.time() - t1)
    
    
    t1 = time.time()
    try:
        print(invoke_with_timeout(1, really_long_function))
        assert(False)
    except BaseException:
        assert(time.time() - t1 < 1.1)
        print("All good", time.time() - t1)
    
    # Checking that classes are referenced and not
    # copied (as would be the case with multiprocessing)
    
    
    class X:
        def __init__(self):
            self.value = 0
    
        def set(self, v):
            self.value = v
    
    
    x = X()
    invoke_with_timeout(2, x.set, 9)
    assert x.value == 9
    
    0 讨论(0)
  • 2020-11-21 05:11

    Great, easy to use and reliable PyPi project timeout-decorator (https://pypi.org/project/timeout-decorator/)

    installation:

    pip install timeout-decorator
    

    Usage:

    import time
    import timeout_decorator
    
    @timeout_decorator.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()
    
    0 讨论(0)
  • 2020-11-21 05:14

    We can use signals for the same. I think the below example will be useful for you. It is very simple compared to threads.

    import signal
    
    def timeout(signum, frame):
        raise myException
    
    #this is an infinite loop, never ending under normal circumstances
    def main():
        print 'Starting Main ',
        while 1:
            print 'in main ',
    
    #SIGALRM is only usable on a unix platform
    signal.signal(signal.SIGALRM, timeout)
    
    #change 5 to however many seconds you need
    signal.alarm(5)
    
    try:
        main()
    except myException:
        print "whoops"
    
    0 讨论(0)
  • 2020-11-21 05:17

    I ran across this thread when searching for a timeout call on unit tests. I didn't find anything simple in the answers or 3rd party packages so I wrote the decorator below you can drop right into code:

    import multiprocessing.pool
    import functools
    
    def timeout(max_timeout):
        """Timeout decorator, parameter in seconds."""
        def timeout_decorator(item):
            """Wrap the original function."""
            @functools.wraps(item)
            def func_wrapper(*args, **kwargs):
                """Closure for function."""
                pool = multiprocessing.pool.ThreadPool(processes=1)
                async_result = pool.apply_async(item, args, kwargs)
                # raises a TimeoutError if execution exceeds max_timeout
                return async_result.get(max_timeout)
            return func_wrapper
        return timeout_decorator
    

    Then it's as simple as this to timeout a test or any function you like:

    @timeout(5.0)  # if execution takes longer than 5 seconds, raise a TimeoutError
    def test_base_regression(self):
        ...
    
    0 讨论(0)
  • 2020-11-21 05:19

    The stopit package, found on pypi, seems to handle timeouts well.

    I like the @stopit.threading_timeoutable decorator, which adds a timeout parameter to the decorated function, which does what you expect, it stops the function.

    Check it out on pypi: https://pypi.python.org/pypi/stopit

    0 讨论(0)
  • 2020-11-21 05:21

    Highlights

    • Raises TimeoutError uses exceptions to alert on timeout - can easily be modified
    • Cross Platform: Windows & Mac OS X
    • Compatibility: Python 3.6+ (I also tested on python 2.7 and it works with small syntax adjustments)

    For full explanation and extension to parallel maps, see here https://flipdazed.github.io/blog/quant%20dev/parallel-functions-with-timeouts

    Minimal Example

    >>> @killer_call(timeout=4)
    ... def bar(x):
    ...        import time
    ...        time.sleep(x)
    ...        return x
    >>> bar(10)
    Traceback (most recent call last):
      ...
    __main__.TimeoutError: function 'bar' timed out after 4s
    

    and as expected

    >>> bar(2)
    2
    

    Full code

    import multiprocessing as mp
    import multiprocessing.queues as mpq
    import functools
    import dill
    
    from typing import Tuple, Callable, Dict, Optional, Iterable, List
    
    class TimeoutError(Exception):
    
        def __init__(self, func, timeout):
            self.t = timeout
            self.fname = func.__name__
    
        def __str__(self):
                return f"function '{self.fname}' timed out after {self.t}s"
    
    
    def _lemmiwinks(func: Callable, args: Tuple[object], kwargs: Dict[str, object], q: mp.Queue):
        """lemmiwinks crawls into the unknown"""
        q.put(dill.loads(func)(*args, **kwargs))
    
    
    def killer_call(func: Callable = None, timeout: int = 10) -> Callable:
        """
        Single function call with a timeout
    
        Args:
            func: the function
            timeout: The timeout in seconds
        """
    
        if not isinstance(timeout, int):
            raise ValueError(f'timeout needs to be an int. Got: {timeout}')
    
        if func is None:
            return functools.partial(killer_call, timeout=timeout)
    
        @functools.wraps(killer_call)
        def _inners(*args, **kwargs) -> object:
            q_worker = mp.Queue()
            proc = mp.Process(target=_lemmiwinks, args=(dill.dumps(func), args, kwargs, q_worker))
            proc.start()
            try:
                return q_worker.get(timeout=timeout)
            except mpq.Empty:
                raise TimeoutError(func, timeout)
            finally:
                try:
                    proc.terminate()
                except:
                    pass
        return _inners
    
    if __name__ == '__main__':
        @killer_call(timeout=4)
        def bar(x):
            import time
            time.sleep(x)
            return x
    
        print(bar(2))
        bar(10)
    

    Notes

    You will need to import inside the function because of the way dill works.

    This will also mean these functions may not be not compatible with doctest if there are imports inside your target functions. You will get an issue with __import__ not found.

    0 讨论(0)
提交回复
热议问题