Timeout a Function


The original idea for this comes from some Python tricks I randomly stumbled across.

The idea is to have a general way to specify a maximum amount of time a function is allowed to run. Consider for example, collecting input from the user – you may give the user some time to enter input but if the time expires you would like to assume some default input.

Let’s start with code that asks the user for a name:

1
2
3
4
5
6
7
8
9
10
import sys
 
def get_name():
    print "Please enter a name: ",
    name = sys.stdin.readline()
    return name
 
if __name__ == '__main__':
    name = get_name()
    print "Got: %s" % name,

Let’s now modify get_name by adding an alarm that trigers in 3 seconds. For this purpose we are going to use Python’s signal library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sys
import signal
 
class TimeoutException(Exception): 
    pass 
 
def get_name():
    def timeout_handler(signum, frame):
        raise TimeoutException()
 
    signal.signal(signal.SIGALRM, timeout_handler) 
    signal.alarm(3) # triger alarm in 3 seconds
 
    try: 
        print "Please enter a name: ",
        name = sys.stdin.readline()
    except TimeoutException:
        return "default value"
    return name
 
if __name__ == '__main__':
    name = get_name()
    print "Got: %s" % name,

The very first thing the example shows is defining our own exception class to use with timeouts. We then see the definition of a timeout handler inside of get_name which simply raises a TimeoutException. Next, we setup the alarm and attempt to get the input from the user. If the alarm triggers before readline() returns then the handler is invoked and an exception is raised. At this point the except clause is executed and the function returns the default value.

Before we go on to generalize this code, lets add a few more lines to clean up after ourselves. If you look at the documentation for signal you will see that signal.signal returns the previous handler. We should reset the handler to the old one after our function is done and we should also make sure to cancel our alarm if it doesn’t trigger using signal.alarm(0):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import sys
import signal
 
class TimeoutException(Exception): 
    pass 
 
def get_name():
    def timeout_handler(signum, frame):
        raise TimeoutException()
 
    old_handler = signal.signal(signal.SIGALRM, timeout_handler) 
    signal.alarm(3) # triger alarm in 3 seconds
 
    try: 
        print "Please enter a name: ",
        name = sys.stdin.readline()
    except TimeoutException:
        return "default value"
    finally:
        signal.signal(signal.SIGALRM, old_handler) 
 
    signal.alarm(0)
    return name
 
if __name__ == '__main__':
    name = get_name()
    print "Got: %s" % name,

We are now ready to proceed with generalizing this function timeout approach. We want the final result to be a [decorator][python/decorators] which we could use with any function as follows:

1
2
3
@timeout(1, "default value")
def get_name()
    ...

This decorator would transform the function into another function which returns the given default value after the specified amount of time. If the time does not expire, it behaves normally.

Our function decorator definition will be a bit more complex since we want to pass arguments to it (the timeout time and default return value). Basically, the timeout function would take as parameters the timeout time and the default return value and it would return another function (timeout_function) which will expect as an argument another function (in our example that would be get_time which is passed automatically as part of the decorator magic). In turn, timeout_function will transform its argument into yet another function which sets up the alarm and behaves as we described earlier.

With all that said here’s the code (we’ve added another function that uses @timeout for demonstration):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import sys
import signal
 
class TimeoutException(Exception): 
    pass 
 
def timeout(timeout_time, default):
    def timeout_function(f):
        def f2(*args):
            def timeout_handler(signum, frame):
                raise TimeoutException()
 
            old_handler = signal.signal(signal.SIGALRM, timeout_handler) 
            signal.alarm(timeout_time) # triger alarm in timeout_time seconds
            try: 
                retval = f()
            except TimeoutException:
                return default
            finally:
                signal.signal(signal.SIGALRM, old_handler) 
            signal.alarm(0)
            return retval
        return f2
    return timeout_function
 
@timeout(3, "default name")
def get_name():
    print "Please enter a name: ",
    name = sys.stdin.readline()
    return name
 
@timeout(1, "default city")
def get_city():
    print "Please enter a city: ",
    city = sys.stdin.readline()
    return city
 
if __name__ == '__main__':
    name = get_name().rstrip("\n")
    city = get_city().rstrip("\n")
    print "Got: %s, %s" % (name, city)

That’s some pretty cool use of Python decorators, wouldn’t you say?