Robert Brewer avatar Robert Brewer committed 49b22a7

New wsgiserver queue timeout, rejecting conns on Queue.Full

Comments (0)

Files changed (4)

cherrypy/_cpserver.py

     socket_file = ''
     socket_queue_size = 5
     socket_timeout = 10
+    accepted_queue_size = -1
+    accepted_queue_timeout = 10
     protocol_version = 'HTTP/1.1'
     reverse_dns = False
     thread_pool = 10

cherrypy/_cpwsgi.py

         s.__init__(self, bind_addr, _cherrypy.tree.apps.items(),
                    server.thread_pool,
                    server.socket_host,
+                   max=server.accepted_queue_size,
                    request_queue_size = server.socket_queue_size,
                    timeout = server.socket_timeout,
+                   accepted_queue_timeout=server.accepted_queue_timeout,
                    )
         self.protocol = server.protocol_version
         self.ssl_certificate = server.ssl_certificate

cherrypy/test/test_conn.py

 from cherrypy.test import test
 test.prefer_parent_path()
 
+import errno
+socket_reset_errors = []
+# Not all of these names will be defined for every platform.
+for _ in ("ECONNRESET", "WSAECONNRESET"):
+    if _ in dir(errno):
+        socket_reset_errors.append(getattr(errno, _))
+
 import httplib
 import socket
 import sys
 import time
+import traceback
 timeout = 0.1
 
 
     cherrypy.tree.mount(Root())
     cherrypy.config.update({
         'server.max_request_body_size': 100,
+        'server.accepted_queue_size': 5,
+        'server.accepted_queue_timeout': 0.1,
         'environment': 'test_suite',
         })
+    import Queue
+    s = cherrypy.server.httpservers.keys()[0]
+    s.requests.maxsize = 5
+    s.accepted_queue_timeout = 0.1
 
 
 from cherrypy.test import helper
         self.assertRaises(httplib.NotConnected, self.getPage, "/")
     
     def test_Streaming_no_len(self):
-        self._streaming(set_cl=False)
+        try:
+            self._streaming(set_cl=False)
+        finally:
+            try:
+                self.HTTP_CONN.close()
+            except (TypeError, AttributeError):
+                pass
     
     def test_Streaming_with_len(self):
-        self._streaming(set_cl=True)
+        try:
+            self._streaming(set_cl=True)
+        finally:
+            try:
+                self.HTTP_CONN.close()
+            except (TypeError, AttributeError):
+                pass
     
     def _streaming(self, set_cl):
         if cherrypy.server.protocol_version == "HTTP/1.1":
         
         self.persistent = True
         conn = self.HTTP_CONN
-        
-        # Try a page without an Expect request header first.
-        # Note that httplib's response.begin automatically ignores
-        # 100 Continue responses, so we must manually check for it.
-        conn.putrequest("POST", "/upload", skip_host=True)
-        conn.putheader("Host", self.HOST)
-        conn.putheader("Content-Type", "text/plain")
-        conn.putheader("Content-Length", "4")
-        conn.endheaders()
-        conn.send("d'oh")
-        response = conn.response_class(conn.sock, method="POST")
-        version, status, reason = response._read_status()
-        self.assertNotEqual(status, 100)
-        conn.close()
-        
-        # Now try a page with an Expect header...
-        conn.connect()
-        conn.putrequest("POST", "/upload", skip_host=True)
-        conn.putheader("Host", self.HOST)
-        conn.putheader("Content-Type", "text/plain")
-        conn.putheader("Content-Length", "17")
-        conn.putheader("Expect", "100-continue")
-        conn.endheaders()
-        response = conn.response_class(conn.sock, method="POST")
-        
-        # ...assert and then skip the 100 response
-        version, status, reason = response._read_status()
-        self.assertEqual(status, 100)
-        while True:
-            line = response.fp.readline().strip()
-            if line:
-                self.fail("100 Continue should not output any headers. Got %r" % line)
-            else:
-                break
-        
-        # ...send the body
-        conn.send("I am a small file")
-        
-        # ...get the final response
-        response.begin()
-        self.status, self.headers, self.body = webtest.shb(response)
-        self.assertStatus(200)
-        self.assertBody("thanks for 'I am a small file' (text/plain)")
+        try:
+            # Try a page without an Expect request header first.
+            # Note that httplib's response.begin automatically ignores
+            # 100 Continue responses, so we must manually check for it.
+            conn.putrequest("POST", "/upload", skip_host=True)
+            conn.putheader("Host", self.HOST)
+            conn.putheader("Content-Type", "text/plain")
+            conn.putheader("Content-Length", "4")
+            conn.endheaders()
+            conn.send("d'oh")
+            response = conn.response_class(conn.sock, method="POST")
+            version, status, reason = response._read_status()
+            self.assertNotEqual(status, 100)
+            conn.close()
+            
+            # Now try a page with an Expect header...
+            conn.connect()
+            conn.putrequest("POST", "/upload", skip_host=True)
+            conn.putheader("Host", self.HOST)
+            conn.putheader("Content-Type", "text/plain")
+            conn.putheader("Content-Length", "17")
+            conn.putheader("Expect", "100-continue")
+            conn.endheaders()
+            response = conn.response_class(conn.sock, method="POST")
+            
+            # ...assert and then skip the 100 response
+            version, status, reason = response._read_status()
+            self.assertEqual(status, 100)
+            while True:
+                skip = response.fp.readline().strip()
+                if not skip:
+                    break
+            
+            # ...send the body
+            conn.send("I am a small file")
+            
+            # ...get the final response
+            response.begin()
+            self.status, self.headers, self.body = webtest.shb(response)
+            self.assertStatus(200)
+            self.assertBody("thanks for 'I am a small file' (text/plain)")
+        finally:
+            conn.close()
     
     def test_No_Message_Body(self):
         if cherrypy.server.protocol_version != "HTTP/1.1":
         # Set our HTTP_CONN to an instance so it persists between requests.
         self.persistent = True
         conn = self.HTTP_CONN
-        
-        # Make the first request and assert there's no "Connection: close".
-        self.getPage("/")
-        self.assertStatus('200 OK')
-        self.assertBody(pov)
-        self.assertNoHeader("Connection")
-        
-        # Make a 204 request on the same connection.
-        self.getPage("/custom/204")
-        self.assertStatus(204)
-        self.assertNoHeader("Content-Length")
-        self.assertBody("")
-        self.assertNoHeader("Connection")
-        
-        # Make a 304 request on the same connection.
-        self.getPage("/custom/304")
-        self.assertStatus(304)
-        self.assertNoHeader("Content-Length")
-        self.assertBody("")
-        self.assertNoHeader("Connection")
+        try:
+            # Make the first request and assert there's no "Connection: close".
+            self.getPage("/")
+            self.assertStatus('200 OK')
+            self.assertBody(pov)
+            self.assertNoHeader("Connection")
+            
+            # Make a 204 request on the same connection.
+            self.getPage("/custom/204")
+            self.assertStatus(204)
+            self.assertNoHeader("Content-Length")
+            self.assertBody("")
+            self.assertNoHeader("Connection")
+            
+            # Make a 304 request on the same connection.
+            self.getPage("/custom/304")
+            self.assertStatus(304)
+            self.assertNoHeader("Content-Length")
+            self.assertBody("")
+            self.assertNoHeader("Connection")
+        finally:
+            conn.close()
     
     def test_Chunked_Encoding(self):
         if cherrypy.server.protocol_version != "HTTP/1.1":
         self.assertBody(pov)
         # Apache, for example, may emit a Connection header even for HTTP/1.0
 ##        self.assertNoHeader("Connection")
+    
+    def test_queue_full(self):
+        conns = []
+        overflow_conn = None
+        
+        try:
+            # Make 15 initial requests and leave them open, which should use
+            # all of wsgiserver's WorkerThreads and fill its Queue.
+            for i in range(15):
+                conn = self.HTTP_CONN(self.HOST, self.PORT)
+                conn.putrequest("POST", "/upload", skip_host=True)
+                conn.putheader("Host", self.HOST)
+                conn.putheader("Content-Type", "text/plain")
+                conn.putheader("Content-Length", "4")
+                conn.endheaders()
+                conns.append(conn)
+            
+            # Now try a 16th conn, which should be closed by the server immediately.
+            overflow_conn = self.HTTP_CONN(self.HOST, self.PORT)
+            # Manually connect since httplib won't let us set a timeout
+            for res in socket.getaddrinfo(self.HOST, self.PORT, 0,
+                                          socket.SOCK_STREAM):
+                af, socktype, proto, canonname, sa = res
+                overflow_conn.sock = socket.socket(af, socktype, proto)
+                overflow_conn.sock.settimeout(5)
+                overflow_conn.sock.connect(sa)
+                break
+            
+            overflow_conn.putrequest("GET", "/", skip_host=True)
+            overflow_conn.putheader("Host", self.HOST)
+            overflow_conn.endheaders()
+            response = overflow_conn.response_class(overflow_conn.sock, method="GET")
+            try:
+                response.begin()
+            except socket.error, exc:
+                if exc.args[0] in socket_reset_errors:
+                    pass
+                else:
+                    raise AssertionError("Overflow conn did not get RST. "
+                                         "Got %s instead" % repr(exc.args))
+            else:
+                raise AssertionError("Overflow conn did not get RST ")
+        finally:
+            for conn in conns:
+                conn.send("done")
+                response = conn.response_class(conn.sock, method="POST")
+                response.begin()
+                self.body = response.read()
+                self.assertBody("thanks for 'done' (text/plain)")
+                self.assertEqual(response.status, 200)
+                conn.close()
+            if overflow_conn:
+                overflow_conn.close()
 
 
 if __name__ == "__main__":

cherrypy/wsgiserver/__init__.py

 
 import errno
 
+def plat_specific_errors(*errnames):
+    """Return error numbers for all errors in errnames on this platform.
+    
+    The 'errno' module contains different global constants depending on
+    the specific platform (OS). This function will return the list of
+    numeric values for a given list of potential names.
+    """
+    errno_names = dir(errno)
+    nums = [getattr(errno, k) for k in errnames if k in errno_names]
+    # de-dupe the list
+    return dict.fromkeys(nums).keys()
+
 socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR")
 
 socket_errors_to_ignore = []
     server_name: the string to set for WSGI's SERVER_NAME environ entry.
         Defaults to socket.gethostname().
     max: the maximum number of queued requests (defaults to -1 = no limit).
+        Someday, we should rename this to "accepted_queue_size".
     request_queue_size: the 'backlog' argument to socket.listen();
         specifies the maximum number of queued connections (default 5).
     timeout: the timeout in seconds for accepted connections (default 10).
+    accepted_queue_timeout: the time, in seconds, to wait for an empty
+        slot in the queue.
     
     protocol: the version string to write in the Status-Line of all
         HTTP responses. For example, "HTTP/1.1" (the default). This
     ssl_private_key = None
     
     def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
-                 max=-1, request_queue_size=5, timeout=10):
+                 max=-1, request_queue_size=5, timeout=10,
+                 accepted_queue_timeout=10):
         self.requests = Queue.Queue(max)
         
         if callable(wsgi_app):
             server_name = socket.gethostname()
         self.server_name = server_name
         self.request_queue_size = request_queue_size
+        self.accepted_queue_timeout = accepted_queue_timeout
         self._workerThreads = []
         
         self.timeout = timeout
             if hasattr(s, 'settimeout'):
                 s.settimeout(self.timeout)
             conn = self.ConnectionClass(s, addr, self)
-            self.requests.put(conn)
+            try:
+                self.requests.put(conn, True, self.accepted_queue_timeout)
+            except Queue.Full:
+                # Just drop the conn. TODO: write 503 back?
+                conn.close()
         except socket.timeout:
             # The only reason for the timeout in start() is so we can
             # notice keyboard interrupts on Win32, which don't interrupt
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.