From 26be023b5831965838312655ea9e9bbf2c6b23e9 Mon Sep 17 00:00:00 2001 From: delthas Date: Wed, 5 Jun 2024 16:12:16 +0200 Subject: [PATCH 1/2] Enable processing concurrent requests in separate threads Closes: https://github.com/csernazs/pytest-httpserver/issues/323 --- pytest_httpserver/httpserver.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pytest_httpserver/httpserver.py b/pytest_httpserver/httpserver.py index 9d6ba1a1..5c4e9381 100644 --- a/pytest_httpserver/httpserver.py +++ b/pytest_httpserver/httpserver.py @@ -598,6 +598,7 @@ class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes :param host: the host or IP where the server will listen :param port: the TCP port where the server will listen :param ssl_context: the ssl context object to use for https connections + :param threaded: whether to handle concurrent requests in separate threads .. py:attribute:: log @@ -619,6 +620,8 @@ def __init__( host: str, port: int, ssl_context: SSLContext | None = None, + *, + threaded: bool = False, ): """ Initializes the instance. @@ -632,6 +635,7 @@ def __init__( self.handler_errors: list[Exception] = [] self.log: list[tuple[Request, Response]] = [] self.ssl_context = ssl_context + self.threaded = threaded self.no_handler_status_code = 500 def __repr__(self): @@ -730,11 +734,11 @@ def start(self): This method returns immediately (e.g. does not block), and it's the caller's responsibility to stop the server (by calling :py:meth:`stop`) when it is no longer needed). - If the sever is not stopped by the caller and execution reaches the end, the + If the server is not stopped by the caller and execution reaches the end, the program needs to be terminated by Ctrl+C or by signal as it will not terminate until the thread is stopped. - If the sever is already running :py:class:`HTTPServerError` will be raised. If you are + If the server is already running :py:class:`HTTPServerError` will be raised. If you are unsure, call :py:meth:`is_running` first. There's a context interface of this class which stops the server when the context block ends. @@ -742,7 +746,9 @@ def start(self): if self.is_running(): raise HTTPServerError("Server is already running") - self.server = make_server(self.host, self.port, self.application, ssl_context=self.ssl_context) + self.server = make_server( + self.host, self.port, self.application, ssl_context=self.ssl_context, threaded=self.threaded + ) self.port = self.server.port # Update port (needed if `port` was set to 0) self.server_thread = threading.Thread(target=self.thread_target) self.server_thread.start() @@ -900,6 +906,8 @@ class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attribute :param default_waiting_settings: the waiting settings object to use as default settings for :py:meth:`wait` context manager + :param threaded: whether to handle concurrent requests in separate threads + .. py:attribute:: no_handler_status_code Attribute containing the http status code (int) which will be the response @@ -916,11 +924,13 @@ def __init__( port=DEFAULT_LISTEN_PORT, ssl_context: SSLContext | None = None, default_waiting_settings: WaitingSettings | None = None, + *, + threaded: bool = False, ): """ Initializes the instance. """ - super().__init__(host, port, ssl_context) + super().__init__(host, port, ssl_context, threaded=threaded) self.ordered_handlers: list[RequestHandler] = [] self.oneshot_handlers = RequestHandlerList() From 3cb7af93686efe65a57d96d5916ec7c0301390a9 Mon Sep 17 00:00:00 2001 From: Cserna Zsolt Date: Sun, 9 Jun 2024 22:26:16 +0200 Subject: [PATCH 2/2] threads: add test for threaded HTTPServer --- tests/test_release.py | 1 + tests/test_threaded.py | 60 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/test_threaded.py diff --git a/tests/test_release.py b/tests/test_release.py index 78aada7a..d26665e3 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -220,6 +220,7 @@ def test_sdist_contents(build: Build, version: str): "test_querystring.py", "test_release.py", "test_ssl.py", + "test_threaded.py", "test_urimatch.py", "test_wait.py", "test_with_statement.py", diff --git a/tests/test_threaded.py b/tests/test_threaded.py new file mode 100644 index 00000000..0a78cd3f --- /dev/null +++ b/tests/test_threaded.py @@ -0,0 +1,60 @@ +import http.client +import threading +import time +from typing import Iterable + +import pytest +from werkzeug.wrappers import Request +from werkzeug.wrappers import Response + +from pytest_httpserver import HTTPServer + + +@pytest.fixture() +def threaded() -> Iterable[HTTPServer]: + server = HTTPServer(threaded=True) + server.start() + yield server + server.clear() + if server.is_running(): + server.stop() + + +def test_threaded(threaded: HTTPServer): + sleep_time = 0.5 + + def handler(_request: Request): + # allow some time to the client to have multiple pending request + # handlers running in parallel + time.sleep(sleep_time) + + # send back thread id + return Response(f"{threading.get_ident()}") + + threaded.expect_request("/foo").respond_with_handler(handler) + + t_start = time.perf_counter() + + number_of_connections = 5 + conns = [http.client.HTTPConnection(threaded.host, threaded.port) for _ in range(number_of_connections)] + + for conn in conns: + conn.request("GET", "/foo", headers={"Host": threaded.host}) + + thread_ids: list[int] = [] + for conn in conns: + response = conn.getresponse() + + assert response.status == 200 + thread_ids.append(int(response.read())) + + for conn in conns: + conn.close() + + t_elapsed = time.perf_counter() - t_start + + assert len(thread_ids) == len(set(thread_ids)), "thread ids returned should be unique" + + assert ( + t_elapsed < number_of_connections * sleep_time * 0.9 + ), "elapsed time should be less than processing sequential requests"