Timeout function if it takes too long to finish [duplicate]
Asked Answered
P

2

176

I have a shell script that loops through a text file containing URL:s that I want to visit and take screenshots of.

All this is done and simple. The script initializes a class that when run creates a screenshot of each site in the list. Some sites take a very, very long time to load, and some might not be loaded at all. So I want to wrap the screengrabber-function in a timeout script, making the function return False if it couldn't finish within 10 seconds.

I'm content with the simplest solution possible, maybe setting a asynchronous timer that will return False after 10 seconds no matter what actually happens inside the function?

Proverbial answered 17/2, 2010 at 15:21 Comment(1)
For all lazy people, who love to use libraries instead of copy+pasting code snippets from StackOverflow: pypi.python.org/pypi/timeout-decoratorProtection
M
286

The process for timing out an operations is described in the documentation for signal.

The basic idea is to use signal handlers to set an alarm for some time interval and raise an exception once that timer expires.

Note that this will only work on UNIX.

Here's an implementation that creates a decorator (save the following code as timeout.py).

import errno
import os
import signal
import functools

class TimeoutError(Exception):
    pass

def timeout(seconds=10, error_message=os.strerror(errno.ETIME)):
    def decorator(func):
        def _handle_timeout(signum, frame):
            raise TimeoutError(error_message)

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            signal.signal(signal.SIGALRM, _handle_timeout)
            signal.alarm(seconds)
            try:
                result = func(*args, **kwargs)
            finally:
                signal.alarm(0)
            return result

        return wrapper

    return decorator

This creates a decorator called @timeout that can be applied to any long running functions.

So, in your application code, you can use the decorator like so:

from timeout import timeout

# Timeout a long running function with the default expiry of 10 seconds.
@timeout
def long_running_function1():
    ...

# Timeout after 5 seconds
@timeout(5)
def long_running_function2():
    ...

# Timeout after 30 seconds, with the error "Connection timed out"
@timeout(30, os.strerror(errno.ETIMEDOUT))
def long_running_function3():
    ...
Marteena answered 17/2, 2010 at 16:56 Comment(13)
Beware that this is not thread-safe: if you're using multithreading, the signal will get caught by a random thread. For single-threaded programs though, this is the easiest solution.Ineradicable
Nice. Also, it is recommended to decorate the function wrapper with @functools.wraps(func)Skid
FYI, there are missing parens after the first "@timeout". It should read @timeout() def ....Clarita
@wim I think it can only be used in main thread, because if you use it in worker thread, it will raise 'ValueError: signal only works in main thread'.Hyperkinesia
Could also use docs.python.org/3/library/signal.html#signal.setitimer to allow half second timeouts.Hedelman
@Ineradicable Could you elaborate on which part is not thread-safe (I imagine something to do with signal) and what side-effects are to be expected ? Also, would you happen to have a threadsafe version ?Lecture
This works great when the function you're decorating doesnt take any arguments. What changes would allow the decorator to be applied to say: def long_running_function4(my_arg):Lejeune
Seems the answer to my question above would be to add empty parenthesis to the decorator as such: @timeout()Lejeune
This is usually working for me, but it occasionally gets stuck on a Jpype call that never returns. Can I do something more forceful like a SIGKILL after 10 seconds?Gastritis
What would be a viable alternative for this, when using threads?Nestling
how would you use this inside a method in another class?Granlund
excellent answer !!!Shadow
This doesn't work on WindowsJefe
U
203

I rewrote David's answer using the with statement, it allows you do do this:

with timeout(seconds=3):
    time.sleep(4)

Which will raise a TimeoutError.

The code is still using signal and thus UNIX only:

import signal

class timeout:
    def __init__(self, seconds=1, error_message='Timeout'):
        self.seconds = seconds
        self.error_message = error_message
    def handle_timeout(self, signum, frame):
        raise TimeoutError(self.error_message)
    def __enter__(self):
        signal.signal(signal.SIGALRM, self.handle_timeout)
        signal.alarm(self.seconds)
    def __exit__(self, type, value, traceback):
        signal.alarm(0)
Uprising answered 12/3, 2014 at 10:40 Comment(11)
Python < v3 does not have a TimeoutError. But one can very easily write one own class with like explained here: https://mcmap.net/q/45062/-proper-way-to-declare-custom-exceptions-in-modern-pythonBenumb
You could easily add in a decorator @timeout.timeout as a static method to this. Then, you could easily choose between a decorator or a with statement.Lines
Interesting to note that if inside the with Timeout(t) context any error is raised, the __exit__ is still called, avoiding, thus, any complication caused by TimeOutError being raised instead of the real error. This is a very lovable solution.Sedition
This is giving me an error if I try to give it a non-integer amount of seconds. Is there a way to make it so it works for floats?Arieariel
@Nick what about docs.python.org/3/library/signal.html#signal.setitimerUprising
Can someone recommend a viable solution that, like this, that works in threads?Nestling
@Nick Some time ago I created version of timeout decorator, that works with floats - #11901828Session
This does not work if the function is catching generic exception, ` def bad_func(): try: time.sleep(100) except Exception: pass `Alysaalyse
Only issue is: signal only works in main threadRest
I suggest using signal.settimer over signal.alarm for the added resolution. With this implementation shortest timeout is 1 second.Laughlin
Same thread issue: signal only works in main thread of the main interpreterCopperas

© 2022 - 2024 — McMap. All rights reserved.