Skip to content

Commit 97a6317

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 97a6317

File tree

2 files changed

+53
-2
lines changed

2 files changed

+53
-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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for the HTTP server."""
22

3+
from http import HTTPStatus
34
import os
45
import queue
56
import socket
@@ -570,3 +571,33 @@ def test_threadpool_multistart_validation(monkeypatch):
570571
match='Threadpools can only be started once.',
571572
):
572573
tp.start()
574+
575+
576+
def test_overload_results_in_suitable_http_error(request):
577+
"""A server that can't keep up with requests returns a 503 HTTP error."""
578+
localhost = "127.0.0.1"
579+
httpserver = HTTPServer(
580+
bind_addr=(localhost, EPHEMERAL_PORT),
581+
gateway=Gateway
582+
)
583+
# Can only handle on request in parallel:
584+
httpserver.requests = ThreadPool(
585+
min=1, max=1, accepted_queue_size=1,
586+
accepted_queue_timeout=0, server=httpserver
587+
)
588+
589+
httpserver.prepare()
590+
serve_thread = threading.Thread(target=httpserver.serve)
591+
serve_thread.start()
592+
request.addfinalizer(httpserver.stop)
593+
# Stop the thread pool to ensure the queue fills up:
594+
httpserver.requests.stop()
595+
596+
_host, port = httpserver.bind_addr
597+
598+
# Use up the very limited thread pool queue we've set up, so future
599+
# requests fail:
600+
httpserver.requests._queue.put(None)
601+
602+
response = requests.get(f"http://127.0.0.1:{port}", timeout=20)
603+
assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE.value

0 commit comments

Comments
 (0)