Time out question

Nick Craig-Wood nick at craig-wood.com
Mon Jul 3 08:30:04 EDT 2006


Alex Martelli <aleax at mac.com> wrote:
>  DarkBlue <nomail at nixmail.com> wrote:
> >  try for 10 seconds
> >    if database.connected :
> >     do your remote thing
> >  except raise after 10 seconds
> >    abort any connection attempt
> >    do something else 
> 
>  Sure, see function setdefaulttimeout in module socket.  Just call it as
>  you wish before trying the DB connection and catch the resulting
>  exception.

It would be nice to have language support for the general case, ie a
general purpose timeout.

In python 2.5 wouldn't it be nice to write

    with timeout(seconds=10) as time_exceeded:
        do potentially time consuming stuff not necessarily involving sockets
    if time_exceeded:
        report an error or whatever

Here is some code I came up with to implement this idea for
python2.3+.  It should be cross platform but I only tested it on
linux.  I think there may still be a threading bug in there (see
FIXME), but it seems to work.  It requires ctypes for access to
PyThreadState_SetAsyncExc).

It is mostly test code as it took absolutely ages to debug!  Threading
code is hard ;-)

............................................................

"""
General purpose timeout mechanism not using alarm(), ie cross platform

Eg

from timeout import Timeout, TimeoutError

def might_infinite_loop(arg):
    while 1:
        pass

try:
    Timeout(10, might_infinite_loop, "some arg")
except TimeoutError:
    print "Oops took too long"
else:
    print "Ran just fine"

"""

import threading
import time
import sys
import ctypes
import os

class TimeoutError(Exception):
    """Thrown on a timeout"""
PyThreadState_SetAsyncExc = ctypes.pythonapi.PyThreadState_SetAsyncExc
_c_TimeoutError = ctypes.py_object(TimeoutError)

class Timeout(threading.Thread):
    """
    A General purpose timeout class
    timeout is int/float in seconds
    action is a callable
    *args, **kwargs are passed to the callable
    """
    def __init__(self, timeout, action, *args, **kwargs):
        threading.Thread.__init__(self)
        self.action = action
        self.args = args
        self.kwargs = kwargs
        self.stopped = False
        self.exc_value = None
        self.end_lock = threading.Lock()
        # start subtask
        self.setDaemon(True)                # FIXME this shouldn't be needed but is, indicating sub tasks aren't ending
        self.start()
        # Wait for subtask to end naturally
        self.join(timeout)
        # Use end_lock to kill the thread in a non-racy
        # fashion. (Using isAlive is racy).  Poking exceptions into
        # the Thread cleanup code isn't a good idea either
        if self.end_lock.acquire(False):
            # gained end_lock => sub thread is still running
            # sub thread is still running so kill it with a TimeoutError
            self.exc_value = TimeoutError()
            PyThreadState_SetAsyncExc(self.id, _c_TimeoutError)
            # release the lock so it can progress into thread cleanup
            self.end_lock.release()
        # shouldn't block since we've killed the thread
        self.join()
        # re-raise any exception
        if self.exc_value:
            raise self.exc_value
    def run(self):
        self.id = threading._get_ident()
        try:
            self.action(*self.args, **self.kwargs)
        except:
            self.exc_value = sys.exc_value
        # only end if we can acquire the end_lock
        self.end_lock.acquire()

if __name__ == "__main__":
    
    def _spin(t):
        """Spins for t seconds"""
        start = time.time()
        end = start + t
        while time.time() < end:
            pass

    def _test_time_limit(name, expecting_time_out, t_limit, fn, *args, **kwargs):
        """Test Timeout"""
        start = time.time()
        
        if expecting_time_out:
            print "Test",name,"should timeout"
        else:
            print "Test",name,"shouldn't timeout"

        try:
            Timeout(t_limit, fn, *args, **kwargs)
        except TimeoutError, e:
            if expecting_time_out:
                print "Timeout generated OK"
            else:
                raise RuntimeError("Wasn't expecting TimeoutError Here")
        else:
            if expecting_time_out:
                raise RuntimeError("Was expecting TimeoutError Here")
            else:
                print "No TimeoutError generated OK"

        elapsed = time.time() - start
        print "That took",elapsed,"seconds for timeout of",t_limit

    def test():
        """Test code"""

        # NB the commented out bits of code don't work with this type
        # of timeout nesting.

        # no nesting
        _test_time_limit("simple #1", True, 5, _spin, 10)
        _test_time_limit("simple #2", False, 10, _spin, 5)

        # 1 level of nesting
        _test_time_limit("nested #1",  True, 4, _test_time_limit,
                         "nested #1a", True, 5, _spin, 10)
        _test_time_limit("nested #2",  False, 6, _test_time_limit,
                         "nested #2a", True, 5, _spin, 10)
        #_test_time_limit("nested #3",  True, 4, _test_time_limit,
        #                 "nested #3a", False, 10, _spin, 5)
        _test_time_limit("nested #4",  False, 6, _test_time_limit,
                         "nested #4a", False, 10, _spin, 5)

        # 2 level of nesting
        _test_time_limit("nested #5",  True, 3, _test_time_limit,
                         "nested #5a", True, 4, _test_time_limit,
                         "nested #5b", True, 5, _spin, 10)
        #_test_time_limit("nested #6",  True, 3, _test_time_limit,
        #                 "nested #6a", False, 6, _test_time_limit,
        #                 "nested #6b", True, 5, _spin, 10)
        #_test_time_limit("nested #7",  True, 3, _test_time_limit,
        #                 "nested #7a", True, 4, _test_time_limit,
        #                 "nested #7b", False, 10, _spin, 5)
        #_test_time_limit("nested #8",  True, 3, _test_time_limit,
        #                 "nested #8a", False, 6, _test_time_limit,
        #                 "nested #8b", False, 10, _spin, 5)
        _test_time_limit("nested #9",  False, 7, _test_time_limit,
                         "nested #9a", True, 4, _test_time_limit,
                         "nested #9b", True, 5, _spin, 10)
        _test_time_limit("nested #10", False, 7, _test_time_limit,
                         "nested #10a",False, 6, _test_time_limit,
                         "nested #10b",True, 5, _spin, 10)
        #_test_time_limit("nested #11", False, 7, _test_time_limit,
        #                 "nested #11a",True, 4, _test_time_limit,
        #                 "nested #11b",False, 10, _spin, 5)
        _test_time_limit("nested #12", False, 7, _test_time_limit,
                         "nested #12a",False, 6, _test_time_limit,
                         "nested #12b",False, 10, _spin, 5)

        print "All tests OK"

    test()


-- 
Nick Craig-Wood <nick at craig-wood.com> -- http://www.craig-wood.com/nick



More information about the Python-list mailing list