threading and timeouts

Phil Mayes nospam at bitbucket.com
Sat Nov 27 08:12:29 CET 1999


Piers Lauder wrote in message <943504970.076.346131773 at cs.usyd.edu.au>...
>Are timeouts possible inside threads?
>
>Eg: escaping from an attempt to use smtplib.SMTP().sendmail() inside a
>child thread if the remote host is being extremely slow.
>
>It doesn't seem possible to use the traditional signal(SIG_ALRM,...)
>approach because signals only work inside the main thread.
>
>The only option I can think of is to re-write the smtplib module to use
>`select', but does anyone know of a simpler solution?


I struggled with this for some time, failed to get plain old timeouts
working, and ended up with a timeout_socket.py that uses select,
based on code from Lloyd Zusman <ljz at asfast.com>.  I then take advantage
of smtplib.py and poplib.py by inheriting:

class timeout_POP3(poplib.POP3):
    def __init__(self, host, port = poplib.POP3_PORT):
        self.host = host
        self.port = port
        self.sock = timeout_socket.timeout_socket()
        self.sock.connect(self.host, self.port)
        self.file = self.sock.makefile('rb') # same as poplib.POP3
        self._debugging = 0
        self.welcome = self._getresp()

class timeout_SMTP(smtplib.SMTP):
    def connect(self, host='localhost', port = 0):
        """Connect to a host on a given port.
        Override the std SMTP connect in order to use a timeout_socket.
        """
        if not port:
            i = string.find(host, ':')
            if i >= 0:
                host, port = host[:i], host[i+1:]
                try: port = string.atoi(port)
                except string.atoi_error:
                    raise socket.error, "nonnumeric port"
        if not port: port = smtplib.SMTP_PORT
        self.sock = timeout_socket.timeout_socket()
        if self.debuglevel > 0: print 'connect:', (host, port)
        self.sock.connect(host, port)
        (code,msg)=self.getreply()
        if self.debuglevel >0 : print "connect:", msg
        return (code,msg)

This allows me to use poplib.py & smtplib.py unchanged.

The timeout_socket class follows.  I run on Win9x and have a vague
feeling that this won't run on Unix which is why I didn't publish it
before.  If you find out why, yell!

------------ code start ----------------------
""" timeout socket class for connections that can potentially cause
    the server to hang
"""
import socket
import errno
import select
import string
#import appstate  # see comment below

_TIMEOUT = 20.0

class Timeout(Exception):
    pass

class Closing(Exception):
    pass

class timeout_socket:
    def __init__(self, timeout=_TIMEOUT, s=None):
        self.timeout = timeout
        self.inbuf = ''
        if s == None:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.s = s
        self.s.setblocking(0)

    # destructor notes: socket.socket will close when destroyed

    def connect(self, host, port):
        timeout = self.timeout
        s = self.s
        try:
            # Non-blocking mode
            s.setblocking(0)
            s.connect(host,port)
            s.setblocking(timeout != 0)
            return 1
        except socket.error,why:
            # The exception arguments can be (string) or (int,string)
            if len(why.args) == 1:
                code = 0
            else:
                code,why = why
            if timeout:
                s.setblocking(1)
                if code in (errno.EINPROGRESS, errno.EALREADY,
errno.EWOULDBLOCK):
                    # Ok, then wait...
                    r,w,e = select.select([],[s],[],timeout)
                    if w:
                        try:
                            s.connect(host, port)
                            return 1
                        except socket.error,why:
                            # This can throw string or (int,string)
                            if len(why.args) == 1:
                                code = 0
                            else:
                                code,why = why
                            if code == errno.EISCONN:
                                return 1
        except:
            code = 0
            why = 'Unknown error trying to connect'

        # format the error message
        if code:
            code = 'error %d = ' % code
        else:
            code = ''
        msg = 'Connect to %s timed out after %d sec (%s%s)' % (host,
int(self.timeout), code, why)
        raise Timeout, msg

    def send(self, data, timeout=0):
        next = 0
        t = timeout or self.timeout
        total = len(data)
        while 1:
            if appstate.state > appstate.running:
                raise Closing, 'closing'
            r,w,e = select.select([],[self.s], [], t)
            if w:
                buf = data[next:next+8192]
                sent = self.s.send(buf)
                next = next + sent
                if next == total:
                    return
            else:
                raise Timeout, 'timeout while sending "%.20s...": %d sec' %
(data, int(t))

    def recv(self, amt, timeout=0):
        t = timeout or self.timeout
        r,w,e = select.select([self.s], [], [], t)
        if r:
            recvd = self.s.recv(amt)
            return recvd
        if not hasattr(self,'s'): print 'timeout_socket has no socket s
?!',self.s #TMP - Sam's problem 9/21
        raise Timeout, "timeout while receiving from %s: %d sec" %
(`self.s.getpeername()`, int(timeout))

    def recvpending(self, timeout=0):
        """ returns 1/0 """
        return [] != select.select([self.s], [], [], timeout or
self.timeout)[0]

    def read(self, amt):
        """ This only returns when amt has been read or socket times out """
        # new version works with readline (except this doesn't convert CRLF
=> LF)
        while 1:
            if appstate.state > appstate.running:
                raise Closing, 'closing'
            if len(self.inbuf) >= amt:
                break
            self.inbuf = self.inbuf + self.recv(4096)
        data = self.inbuf[:amt]
        self.inbuf = self.inbuf[amt:]
        return data

    # new readline buffers data
    # this -isn't- called for the makefile clone
    def readline(self):
        while 1:
# useful but non-generic code:
#            if appstate.state > appstate.running:
#                raise Closing, 'closing'
            crlf = string.find(self.inbuf, '\r\n')
            if crlf >= 0:
                break
            self.inbuf = self.inbuf + self.recv(4096)
        data = self.inbuf[:crlf] + '\n'
        self.inbuf = self.inbuf[crlf+2:]
        return data

    def close(self):
        self.s.close()

    def makefile(self, flags):
        self.s.setblocking(1) # s is a socket._socketobject
        save = self.s._sock   # stash the real thing
        self.s._sock = self   # replace it by us
        # this call creates a _fileobject that has been passed self as the
socket
        x = self.s.makefile(flags)
        self.s._sock = save   # restore the real socket
        return x

------------ code end ------------------------
--
Phil Mayes    pmayes AT olivebr DOT com
Olive Branch Software - home of Arranger
http://www.olivebr.com/







More information about the Python-list mailing list