changeset 118:00a660fd9cb4

Add configurable timeout (default: no timeout) to be used when the WSGI application is called. Only applies to forked servers!
author Allan Saddi <allan@saddi.com>
date Wed, 21 Oct 2009 09:29:33 -0700
parents 6ea1ffac1bcb
children 9eecf1c4e416 57375deb17c3
files ChangeLog flup/server/ajp.py flup/server/ajp_base.py flup/server/ajp_fork.py flup/server/fcgi.py flup/server/fcgi_base.py flup/server/fcgi_fork.py flup/server/scgi.py flup/server/scgi_base.py flup/server/scgi_fork.py
diffstat 10 files changed, 91 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
     1.1 --- a/ChangeLog	Mon Aug 17 12:42:43 2009 -0700
     1.2 +++ b/ChangeLog	Wed Oct 21 09:29:33 2009 -0700
     1.3 @@ -1,3 +1,8 @@
     1.4 +2009-10-21  Allan Saddi  <allan@saddi.com>
     1.5 +
     1.6 +	* Add configurable timeout (default: no timeout) to be used when the
     1.7 +	  WSGI application is called. Only applies to forked servers!
     1.8 +
     1.9  2009-06-05  Allan Saddi  <allan@saddi.com>
    1.10  
    1.11  	* Fix bug in scgi servers that occurs when SCRIPT_NAME is missing.
     2.1 --- a/flup/server/ajp.py	Mon Aug 17 12:42:43 2009 -0700
     2.2 +++ b/flup/server/ajp.py	Wed Oct 21 09:29:33 2009 -0700
     2.3 @@ -142,8 +142,8 @@
     2.4          for key in ('jobClass', 'jobArgs'):
     2.5              if kw.has_key(key):
     2.6                  del kw[key]
     2.7 -        ThreadedServer.__init__(self, jobClass=Connection, jobArgs=(self,),
     2.8 -                                **kw)
     2.9 +        ThreadedServer.__init__(self, jobClass=Connection,
    2.10 +                                jobArgs=(self, None), **kw)
    2.11  
    2.12      def run(self):
    2.13          """
     3.1 --- a/flup/server/ajp_base.py	Mon Aug 17 12:42:43 2009 -0700
     3.2 +++ b/flup/server/ajp_base.py	Wed Oct 21 09:29:33 2009 -0700
     3.3 @@ -36,6 +36,7 @@
     3.4  import errno
     3.5  import datetime
     3.6  import time
     3.7 +import traceback
     3.8  
     3.9  # Unfortunately, for now, threads are required.
    3.10  import thread
    3.11 @@ -591,21 +592,31 @@
    3.12              data = data[toWrite:]
    3.13              bytesLeft -= toWrite
    3.14  
    3.15 +class TimeoutException(Exception):
    3.16 +    pass
    3.17 +
    3.18  class Connection(object):
    3.19      """
    3.20      A single Connection with the server. Requests are not multiplexed over the
    3.21      same connection, so at any given time, the Connection is either
    3.22      waiting for a request, or processing a single request.
    3.23      """
    3.24 -    def __init__(self, sock, addr, server):
    3.25 +    def __init__(self, sock, addr, server, timeout):
    3.26          self.server = server
    3.27          self._sock = sock
    3.28          self._addr = addr
    3.29 +        self._timeout = timeout
    3.30  
    3.31          self._request = None
    3.32  
    3.33          self.logger = logging.getLogger(LoggerName)
    3.34  
    3.35 +    def timeout_handler(self, signum, frame):
    3.36 +        self.logger.error('Timeout Exceeded')
    3.37 +        self.logger.error("\n".join(traceback.format_stack(frame)))
    3.38 +
    3.39 +        raise TimeoutException
    3.40 +
    3.41      def run(self):
    3.42          self.logger.debug('Connection starting up (%s:%d)',
    3.43                            self._addr[0], self._addr[1])
    3.44 @@ -702,11 +713,20 @@
    3.45          if req.input.bytesAvailForAdd():
    3.46              self.processInput()
    3.47  
    3.48 +        # If there is a timeout
    3.49 +        if self._timeout:
    3.50 +            old_alarm = signal.signal(signal.SIGALRM, self.timeout_handler)
    3.51 +            signal.alarm(self._timeout)
    3.52 +            
    3.53          # Run Request.
    3.54          req.run()
    3.55  
    3.56          self._request = None
    3.57  
    3.58 +        # Restore old handler if timeout was given
    3.59 +        if self._timeout:
    3.60 +            signal.signal(signal.SIGALRM, old_alarm)
    3.61 +
    3.62      def _shutdown(self, pkt):
    3.63          """Not sure what to do with this yet."""
    3.64          self.logger.info('Received shutdown request from server')
     4.1 --- a/flup/server/ajp_fork.py	Mon Aug 17 12:42:43 2009 -0700
     4.2 +++ b/flup/server/ajp_fork.py	Wed Oct 21 09:29:33 2009 -0700
     4.3 @@ -108,7 +108,7 @@
     4.4      """
     4.5      def __init__(self, application, scriptName='', environ=None,
     4.6                   bindAddress=('localhost', 8009), allowedServers=None,
     4.7 -                 loggingLevel=logging.INFO, debug=False, **kw):
     4.8 +                 loggingLevel=logging.INFO, debug=False, timeout=None, **kw):
     4.9          """
    4.10          scriptName is the initial portion of the URL path that "belongs"
    4.11          to your application. It is used to determine PATH_INFO (which doesn't
    4.12 @@ -141,7 +141,8 @@
    4.13          for key in ('multithreaded', 'multiprocess', 'jobClass', 'jobArgs'):
    4.14              if kw.has_key(key):
    4.15                  del kw[key]
    4.16 -        PreforkServer.__init__(self, jobClass=Connection, jobArgs=(self,), **kw)
    4.17 +        PreforkServer.__init__(self, jobClass=Connection,
    4.18 +                               jobArgs=(self, timeout), **kw)
    4.19  
    4.20      def run(self):
    4.21          """
     5.1 --- a/flup/server/fcgi.py	Mon Aug 17 12:42:43 2009 -0700
     5.2 +++ b/flup/server/fcgi.py	Wed Oct 21 09:29:33 2009 -0700
     5.3 @@ -93,7 +93,7 @@
     5.4              if kw.has_key(key):
     5.5                  del kw[key]
     5.6          ThreadedServer.__init__(self, jobClass=self._connectionClass,
     5.7 -                                jobArgs=(self,), **kw)
     5.8 +                                jobArgs=(self, None), **kw)
     5.9  
    5.10      def _isClientAllowed(self, addr):
    5.11          return self._web_server_addrs is None or \
     6.1 --- a/flup/server/fcgi_base.py	Mon Aug 17 12:42:43 2009 -0700
     6.2 +++ b/flup/server/fcgi_base.py	Wed Oct 21 09:29:33 2009 -0700
     6.3 @@ -533,6 +533,9 @@
     6.4          if self.paddingLength:
     6.5              self._sendall(sock, '\x00'*self.paddingLength)
     6.6              
     6.7 +class TimeoutException(Exception):
     6.8 +    pass
     6.9 +
    6.10  class Request(object):
    6.11      """
    6.12      Represents a single FastCGI request.
    6.13 @@ -542,8 +545,9 @@
    6.14      be called by your handler. However, server, params, stdin, stdout,
    6.15      stderr, and data are free for your handler's use.
    6.16      """
    6.17 -    def __init__(self, conn, inputStreamClass):
    6.18 +    def __init__(self, conn, inputStreamClass, timeout):
    6.19          self._conn = conn
    6.20 +        self._timeout = timeout
    6.21  
    6.22          self.server = conn.server
    6.23          self.params = {}
    6.24 @@ -552,8 +556,20 @@
    6.25          self.stderr = OutputStream(conn, self, FCGI_STDERR, buffered=True)
    6.26          self.data = inputStreamClass(conn)
    6.27  
    6.28 +    def timeout_handler(self, signum, frame):
    6.29 +        self.stderr.write('Timeout Exceeded\n')
    6.30 +        self.stderr.write("\n".join(traceback.format_stack(frame)))
    6.31 +        self.stderr.flush()
    6.32 +
    6.33 +        raise TimeoutException
    6.34 +
    6.35      def run(self):
    6.36          """Runs the handler, flushes the streams, and ends the request."""
    6.37 +        # If there is a timeout
    6.38 +        if self._timeout:
    6.39 +            old_alarm = signal.signal(signal.SIGALRM, self.timeout_handler)
    6.40 +            signal.alarm(self._timeout)
    6.41 +            
    6.42          try:
    6.43              protocolStatus, appStatus = self.server.handler(self)
    6.44          except:
    6.45 @@ -567,6 +583,10 @@
    6.46          if __debug__: _debug(1, 'protocolStatus = %d, appStatus = %d' %
    6.47                               (protocolStatus, appStatus))
    6.48  
    6.49 +        # Restore old handler if timeout was given
    6.50 +        if self._timeout:
    6.51 +            signal.signal(signal.SIGALRM, old_alarm)
    6.52 +
    6.53          try:
    6.54              self._flush()
    6.55              self._end(appStatus, protocolStatus)
    6.56 @@ -615,10 +635,11 @@
    6.57      _multiplexed = False
    6.58      _inputStreamClass = InputStream
    6.59  
    6.60 -    def __init__(self, sock, addr, server):
    6.61 +    def __init__(self, sock, addr, server, timeout):
    6.62          self._sock = sock
    6.63          self._addr = addr
    6.64          self.server = server
    6.65 +        self._timeout = timeout
    6.66  
    6.67          # Active Requests for this Connection, mapped by request ID.
    6.68          self._requests = {}
    6.69 @@ -739,7 +760,8 @@
    6.70          """Handle an FCGI_BEGIN_REQUEST from the web server."""
    6.71          role, flags = struct.unpack(FCGI_BeginRequestBody, inrec.contentData)
    6.72  
    6.73 -        req = self.server.request_class(self, self._inputStreamClass)
    6.74 +        req = self.server.request_class(self, self._inputStreamClass,
    6.75 +                                        self._timeout)
    6.76          req.requestId, req.role, req.flags = inrec.requestId, role, flags
    6.77          req.aborted = False
    6.78  
    6.79 @@ -807,8 +829,9 @@
    6.80      _multiplexed = True
    6.81      _inputStreamClass = MultiplexedInputStream
    6.82  
    6.83 -    def __init__(self, sock, addr, server):
    6.84 -        super(MultiplexedConnection, self).__init__(sock, addr, server)
    6.85 +    def __init__(self, sock, addr, server, timeout):
    6.86 +        super(MultiplexedConnection, self).__init__(sock, addr, server,
    6.87 +                                                    timeout)
    6.88  
    6.89          # Used to arbitrate access to self._requests.
    6.90          lock = threading.RLock()
     7.1 --- a/flup/server/fcgi_fork.py	Mon Aug 17 12:42:43 2009 -0700
     7.2 +++ b/flup/server/fcgi_fork.py	Wed Oct 21 09:29:33 2009 -0700
     7.3 @@ -70,7 +70,8 @@
     7.4      """
     7.5      def __init__(self, application, environ=None,
     7.6                   bindAddress=None, umask=None, multiplexed=False,
     7.7 -                 debug=False, roles=(FCGI_RESPONDER,), forceCGI=False, **kw):
     7.8 +                 debug=False, roles=(FCGI_RESPONDER,), forceCGI=False,
     7.9 +                 timeout=None, **kw):
    7.10          """
    7.11          environ, if present, must be a dictionary-like object. Its
    7.12          contents will be copied into application's environ. Useful
    7.13 @@ -99,7 +100,7 @@
    7.14              if kw.has_key(key):
    7.15                  del kw[key]
    7.16          PreforkServer.__init__(self, jobClass=self._connectionClass,
    7.17 -                               jobArgs=(self,), **kw)
    7.18 +                               jobArgs=(self, timeout), **kw)
    7.19  
    7.20          try:
    7.21              import resource
     8.1 --- a/flup/server/scgi.py	Mon Aug 17 12:42:43 2009 -0700
     8.2 +++ b/flup/server/scgi.py	Wed Oct 21 09:29:33 2009 -0700
     8.3 @@ -138,8 +138,8 @@
     8.4          for key in ('jobClass', 'jobArgs'):
     8.5              if kw.has_key(key):
     8.6                  del kw[key]
     8.7 -        ThreadedServer.__init__(self, jobClass=Connection, jobArgs=(self,),
     8.8 -                                **kw)
     8.9 +        ThreadedServer.__init__(self, jobClass=Connection,
    8.10 +                                jobArgs=(self, None), **kw)
    8.11  
    8.12      def run(self):
    8.13          """
     9.1 --- a/flup/server/scgi_base.py	Mon Aug 17 12:42:43 2009 -0700
     9.2 +++ b/flup/server/scgi_base.py	Wed Oct 21 09:29:33 2009 -0700
     9.3 @@ -37,6 +37,7 @@
     9.4  import datetime
     9.5  import os
     9.6  import warnings
     9.7 +import traceback
     9.8  
     9.9  # Threads are required. If you want a non-threaded (forking) version, look at
    9.10  # SWAP <http://www.idyll.org/~t/www-tools/wsgi/>.
    9.11 @@ -197,18 +198,28 @@
    9.12                            handlerTime.seconds +
    9.13                            handlerTime.microseconds / 1000000.0)
    9.14  
    9.15 +class TimeoutException(Exception):
    9.16 +    pass
    9.17 +
    9.18  class Connection(object):
    9.19      """
    9.20      Represents a single client (web server) connection. A single request
    9.21      is handled, after which the socket is closed.
    9.22      """
    9.23 -    def __init__(self, sock, addr, server):
    9.24 +    def __init__(self, sock, addr, server, timeout):
    9.25          self._sock = sock
    9.26          self._addr = addr
    9.27          self.server = server
    9.28 +        self._timeout = timeout
    9.29  
    9.30          self.logger = logging.getLogger(LoggerName)
    9.31  
    9.32 +    def timeout_handler(self, signum, frame):
    9.33 +        self.logger.error('Timeout Exceeded')
    9.34 +        self.logger.error("\n".join(traceback.format_stack(frame)))
    9.35 +
    9.36 +        raise TimeoutException
    9.37 +
    9.38      def run(self):
    9.39          if len(self._addr) == 2:
    9.40              self.logger.debug('Connection starting up (%s:%d)',
    9.41 @@ -263,12 +274,22 @@
    9.42          # Allocate Request
    9.43          req = Request(self, environ, input, output)
    9.44  
    9.45 +        # If there is a timeout
    9.46 +        if self._timeout:
    9.47 +            old_alarm = signal.signal(signal.SIGALRM, self.timeout_handler)
    9.48 +            signal.alarm(self._timeout)
    9.49 +            
    9.50          # Run it.
    9.51          req.run()
    9.52  
    9.53          output.close()
    9.54          input.close()
    9.55  
    9.56 +        # Restore old handler if timeout was given
    9.57 +        if self._timeout:
    9.58 +            signal.signal(signal.SIGALRM, old_alarm)
    9.59 +
    9.60 +
    9.61  class BaseSCGIServer(object):
    9.62      # What Request class to use.
    9.63      requestClass = Request
    10.1 --- a/flup/server/scgi_fork.py	Mon Aug 17 12:42:43 2009 -0700
    10.2 +++ b/flup/server/scgi_fork.py	Wed Oct 21 09:29:33 2009 -0700
    10.3 @@ -97,7 +97,7 @@
    10.4      def __init__(self, application, scriptName=NoDefault, environ=None,
    10.5                   bindAddress=('localhost', 4000), umask=None,
    10.6                   allowedServers=None,
    10.7 -                 loggingLevel=logging.INFO, debug=False, **kw):
    10.8 +                 loggingLevel=logging.INFO, debug=False, timeout=None, **kw):
    10.9          """
   10.10          scriptName is the initial portion of the URL path that "belongs"
   10.11          to your application. It is used to determine PATH_INFO (which doesn't
   10.12 @@ -137,7 +137,9 @@
   10.13          for key in ('multithreaded', 'multiprocess', 'jobClass', 'jobArgs'):
   10.14              if kw.has_key(key):
   10.15                  del kw[key]
   10.16 -        PreforkServer.__init__(self, jobClass=Connection, jobArgs=(self,), **kw)
   10.17 +        
   10.18 +        PreforkServer.__init__(self, jobClass=Connection,
   10.19 +                               jobArgs=(self, timeout), **kw)
   10.20  
   10.21      def run(self):
   10.22          """