Skip to content

Commit 57206a1

Browse files
committed
When connection queue is full, respond with a 503 error
The 503 responses are run in a thread
1 parent c257742 commit 57206a1

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

cheroot/server.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1658,6 +1658,8 @@ def __init__(
16581658
self.reuse_port = reuse_port
16591659
self.clear_stats()
16601660

1661+
self._unservicable_conns = queue.Queue()
1662+
16611663
def clear_stats(self):
16621664
"""Reset server stat counters.."""
16631665
self._start_time = None
@@ -1866,8 +1868,26 @@ def prepare(self): # noqa: C901 # FIXME
18661868
self.ready = True
18671869
self._start_time = time.time()
18681870

1871+
def _serve_unservicable(self):
1872+
"""Serve connections we can't handle a 503."""
1873+
while self.ready:
1874+
conn = self._unservicable_conns.get()
1875+
if conn is None:
1876+
return
1877+
request = HTTPRequest(self, conn)
1878+
try:
1879+
request.simple_response("503 Service Unavailable")
1880+
except Exception as ex:
1881+
self.server.error_log(
1882+
repr(ex),
1883+
level=logging.ERROR,
1884+
traceback=True,
1885+
)
1886+
conn.close()
1887+
18691888
def serve(self):
18701889
"""Serve requests, after invoking :func:`prepare()`."""
1890+
threading.Thread(target=self._serve_unservicable).start()
18711891
while self.ready and not self.interrupt:
18721892
try:
18731893
self._connections.run(self.expiration_interval)
@@ -2162,8 +2182,7 @@ def process_conn(self, conn):
21622182
try:
21632183
self.requests.put(conn)
21642184
except queue.Full:
2165-
# Just drop the conn. TODO: write 503 back?
2166-
conn.close()
2185+
self._unservicable_conns.put(conn)
21672186

21682187
@property
21692188
def interrupt(self):
@@ -2201,6 +2220,7 @@ def stop(self): # noqa: C901 # FIXME
22012220
return # already stopped
22022221

22032222
self.ready = False
2223+
self._unservicable_conns.put(None)
22042224
if self._start_time is not None:
22052225
self._run_time += time.time() - self._start_time
22062226
self._start_time = None

cheroot/test/test_server.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,3 +570,32 @@ def test_threadpool_multistart_validation(monkeypatch):
570570
match='Threadpools can only be started once.',
571571
):
572572
tp.start()
573+
574+
575+
def test_overload_results_in_suitable_http_error(request):
576+
"""A server that can't keep up with requests returns a 503 HTTP error."""
577+
httpserver = HTTPServer(
578+
bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT),
579+
gateway=Gateway
580+
)
581+
# Can only handle on request in parallel:
582+
httpserver.requests = ThreadPool(
583+
min=1, max=1, accepted_queue_size=1,
584+
accepted_queue_timeout=0, server=httpserver
585+
)
586+
587+
httpserver.prepare()
588+
serve_thread = threading.Thread(target=httpserver.serve)
589+
serve_thread.start()
590+
request.addfinalizer(httpserver.stop)
591+
# Stop the thread pool to ensure the queue fills up:
592+
httpserver.requests.stop()
593+
594+
host, port = httpserver.bind_addr
595+
596+
# Use up the very limited thread pool queue we've set up, so future
597+
# requests fail:
598+
httpserver.requests._queue.put(None)
599+
600+
response = requests.get(f"http://{host}:{port}", timeout=5)
601+
assert response.status_code == 503

0 commit comments

Comments
 (0)