119 lines
3.8 KiB
Python
119 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Concurrency-bounded threaded HTTP server for the Hermes SCM broker."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
from http.server import ThreadingHTTPServer
|
|
|
|
MAX_CONCURRENT_REQUESTS = 3
|
|
|
|
|
|
class _AbsoluteDeadlineReader:
|
|
"""Read header lines against one wall-clock deadline, not idle timeouts."""
|
|
|
|
def __init__(self, stream, connection):
|
|
self._stream = stream
|
|
self._connection = connection
|
|
self._deadline: float | None = None
|
|
|
|
def begin(self, timeout: float) -> None:
|
|
self._deadline = time.monotonic() + timeout
|
|
|
|
def end(self) -> None:
|
|
self._deadline = None
|
|
|
|
def readline(self, limit: int = -1) -> bytes:
|
|
if self._deadline is None:
|
|
return self._stream.readline(limit)
|
|
value = bytearray()
|
|
while limit < 0 or len(value) < limit:
|
|
remaining = self._deadline - time.monotonic()
|
|
if remaining <= 0:
|
|
raise TimeoutError("absolute SCM header deadline exceeded")
|
|
self._connection.settimeout(remaining)
|
|
character = self._stream.read(1)
|
|
if not character:
|
|
break
|
|
value.extend(character)
|
|
if character == b"\n":
|
|
break
|
|
return bytes(value)
|
|
|
|
def __getattr__(self, name: str):
|
|
return getattr(self._stream, name)
|
|
|
|
|
|
class AbsoluteHeaderDeadlineMixin:
|
|
"""Apply an absolute request-line plus header deadline to HTTP handlers."""
|
|
|
|
header_deadline_seconds = 10.0
|
|
|
|
def setup(self) -> None:
|
|
super().setup()
|
|
self._header_reader = _AbsoluteDeadlineReader(self.rfile, self.connection)
|
|
self.rfile = self._header_reader
|
|
|
|
def handle_one_request(self) -> None:
|
|
try:
|
|
self._header_reader.begin(self.header_deadline_seconds)
|
|
try:
|
|
self.raw_requestline = self.rfile.readline(65537)
|
|
if len(self.raw_requestline) > 65536:
|
|
self.requestline = ""
|
|
self.request_version = ""
|
|
self.command = ""
|
|
self.send_error(414)
|
|
return
|
|
if not self.raw_requestline:
|
|
self.close_connection = True
|
|
return
|
|
if not self.parse_request():
|
|
return
|
|
finally:
|
|
self._header_reader.end()
|
|
method_name = "do_" + self.command
|
|
if not hasattr(self, method_name):
|
|
self.send_error(501, "Unsupported method")
|
|
return
|
|
getattr(self, method_name)()
|
|
self.wfile.flush()
|
|
except TimeoutError:
|
|
self.close_connection = True
|
|
|
|
|
|
class BoundedThreadingHTTPServer(ThreadingHTTPServer):
|
|
"""Cap active handlers so large Git exchanges cannot exhaust memory."""
|
|
|
|
daemon_threads = True
|
|
block_on_close = True
|
|
request_queue_size = 16
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self._slots = threading.BoundedSemaphore(MAX_CONCURRENT_REQUESTS)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def process_request(self, request, client_address) -> None:
|
|
if not self._slots.acquire(blocking=False):
|
|
try:
|
|
request.sendall(
|
|
b"HTTP/1.1 503 Service Unavailable\r\n"
|
|
b"Connection: close\r\nContent-Length: 0\r\n\r\n"
|
|
)
|
|
finally:
|
|
self.shutdown_request(request)
|
|
return
|
|
try:
|
|
super().process_request(request, client_address)
|
|
except Exception:
|
|
self._slots.release()
|
|
self.shutdown_request(request)
|
|
raise
|
|
|
|
def process_request_thread(self, request, client_address) -> None:
|
|
try:
|
|
super().process_request_thread(request, client_address)
|
|
finally:
|
|
self._slots.release()
|