Skip to content

Commit dab349d

Browse files
pythonspeedwebknjaz
authored andcommitted
When the connection queue is full, respond with a 503 error
The 503 responses are run in a thread
1 parent fdaa24d commit dab349d

File tree

2 files changed

+73
-2
lines changed

2 files changed

+73
-2
lines changed

cheroot/server.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,11 @@ def _close_kernel_socket(self):
15201520
raise
15211521

15221522

1523+
# Unique identifier used to indicate the thread that handles unservicable
1524+
# connections should shut down.
1525+
_SHUT_DOWN_UNSERVICABLES_THREAD = object()
1526+
1527+
15231528
class HTTPServer:
15241529
"""An HTTP server."""
15251530

@@ -1658,6 +1663,8 @@ def __init__(
16581663
self.reuse_port = reuse_port
16591664
self.clear_stats()
16601665

1666+
self._unservicable_conns = queue.Queue()
1667+
16611668
def clear_stats(self):
16621669
"""Reset server stat counters.."""
16631670
self._start_time = None
@@ -1866,8 +1873,34 @@ def prepare(self): # noqa: C901 # FIXME
18661873
self.ready = True
18671874
self._start_time = time.time()
18681875

1876+
def _serve_unservicable(self):
1877+
"""Serve connections we can't handle a 503."""
1878+
while self.ready:
1879+
conn = self._unservicable_conns.get()
1880+
if conn is _SHUT_DOWN_UNSERVICABLES_THREAD:
1881+
return
1882+
request = HTTPRequest(self, conn)
1883+
try:
1884+
request.simple_response('503 Service Unavailable')
1885+
except (socket.error, errors.FatalSSLAlert):
1886+
# We're sending the 503 error to be polite, it it fails that's
1887+
# fine.
1888+
continue
1889+
except Exception as ex:
1890+
self.server.error_log(
1891+
repr(ex),
1892+
level=logging.ERROR,
1893+
traceback=True,
1894+
)
1895+
conn.close()
1896+
18691897
def serve(self):
18701898
"""Serve requests, after invoking :func:`prepare()`."""
1899+
# This thread will handle unservicable connections, as added to
1900+
# self._unservicable_conns queue. It will run forever, until
1901+
# self.stop() tells it to shut down.
1902+
threading.Thread(target=self._serve_unservicable).start()
1903+
18711904
while self.ready and not self.interrupt:
18721905
try:
18731906
self._connections.run(self.expiration_interval)
@@ -2162,8 +2195,7 @@ def process_conn(self, conn):
21622195
try:
21632196
self.requests.put(conn)
21642197
except queue.Full:
2165-
# Just drop the conn. TODO: write 503 back?
2166-
conn.close()
2198+
self._unservicable_conns.put(conn)
21672199

21682200
@property
21692201
def interrupt(self):
@@ -2201,6 +2233,11 @@ def stop(self): # noqa: C901 # FIXME
22012233
return # already stopped
22022234

22032235
self.ready = False
2236+
2237+
# This tells the thread that handles unservicable connections to shut
2238+
# down:
2239+
self._unservicable_conns.put(_SHUT_DOWN_UNSERVICABLES_THREAD)
2240+
22042241
if self._start_time is not None:
22052242
self._run_time += time.time() - self._start_time
22062243
self._start_time = None

cheroot/test/test_server.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import types
99
import urllib.parse # noqa: WPS301
1010
import uuid
11+
from http import HTTPStatus
1112

1213
import pytest
1314

@@ -570,3 +571,36 @@ 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,
586+
max=1,
587+
accepted_queue_size=1,
588+
accepted_queue_timeout=0,
589+
server=httpserver,
590+
)
591+
592+
httpserver.prepare()
593+
serve_thread = threading.Thread(target=httpserver.serve)
594+
serve_thread.start()
595+
request.addfinalizer(httpserver.stop)
596+
# Stop the thread pool to ensure the queue fills up:
597+
httpserver.requests.stop()
598+
599+
_host, port = httpserver.bind_addr
600+
601+
# Use up the very limited thread pool queue we've set up, so future
602+
# requests fail:
603+
httpserver.requests._queue.put(None)
604+
605+
response = requests.get(f'http://{localhost}:{port}', timeout=20)
606+
assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE

0 commit comments

Comments
 (0)