From 0e112b6fd89a9651a1c1fd12285457d14756574d Mon Sep 17 00:00:00 2001 From: TAHRI Ahmed R Date: Mon, 6 Nov 2023 20:19:51 +0100 Subject: [PATCH] :bookmark: Release 3.2.1 (#41) **Fixed** - Performance issues in HTTP/2, and HTTP/3, with or without multiplexed connections. **Changed** - Enforced a maximum in-flight request when using multiplexed connections. Default to 200 per connections so, actually 2000 per Session (_default is 10 connections_). This can be overriden in our `HTTPAdapter` for advanced users. --- HISTORY.md | 10 ++++++++++ README.md | 6 +++++- src/niquests/__version__.py | 4 ++-- src/niquests/adapters.py | 35 +++++++++++++++++++++++++++-------- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index d9dbde6b74..bee34080d6 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,16 @@ Release History =============== +3.2.1 (2023-11-06) +------------------ + +**Fixed** +- Performance issues in HTTP/2, and HTTP/3, with or without multiplexed connections. + +**Changed** +- Enforced a maximum in-flight request when using multiplexed connections. Default to 200 per connections + so, actually 2000 per Session (_default is 10 connections_). This can be overriden in our `HTTPAdapter` for advanced users. + 3.2.0 (2023-11-05) ------------------ diff --git a/README.md b/README.md index 1710db2c16..7c29182531 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **Niquests** is a simple, yet elegant, HTTP library. It is a drop-in replacement for **Requests** that is no longer under feature freeze. -Niquests, is the “**Safest**, **Fastest**, **Easiest**, and **Most advanced**” Python HTTP Client. +Niquests, is the “**Safest**, **Fastest***, **Easiest**, and **Most advanced**” Python HTTP Client. ```python >>> import niquests @@ -73,4 +73,8 @@ We intend to keep it that way. As long as we can, long live Niquests! --- +(*) performance measured when leveraging a multiplexed connection with or without uses of any form of concurrency as of november 2023. + +--- + [![Kenneth Reitz](https://raw.githubusercontent.com/jawah/niquests/main/ext/kr.png)](https://kennethreitz.org) [![Python Software Foundation](https://raw.githubusercontent.com/psf/requests/main/ext/psf.png)](https://www.python.org/psf) diff --git a/src/niquests/__version__.py b/src/niquests/__version__.py index ff16bf1c1f..b88eb89f01 100644 --- a/src/niquests/__version__.py +++ b/src/niquests/__version__.py @@ -9,9 +9,9 @@ __url__: str = "https://niquests.readthedocs.io" __version__: str -__version__ = "3.2.0" +__version__ = "3.2.1" -__build__: int = 0x030200 +__build__: int = 0x030201 __author__: str = "Kenneth Reitz" __author_email__: str = "me@kennethreitz.org" __license__: str = "Apache-2.0" diff --git a/src/niquests/adapters.py b/src/niquests/adapters.py index bcaa16ded2..f428c01128 100644 --- a/src/niquests/adapters.py +++ b/src/niquests/adapters.py @@ -190,6 +190,7 @@ def __init__( quic_cache_layer: CacheLayerAltSvcType | None = None, disable_http2: bool = False, disable_http3: bool = False, + max_in_flight_multiplexed: int | None = None, ): if isinstance(max_retries, bool): self.max_retries: RetryType = False @@ -217,7 +218,12 @@ def __init__( self._disable_http3 = disable_http3 #: we keep a list of pending (lazy) response - self._promises: list[Response] = [] + self._promises: dict[str, Response] = {} + self._max_in_flight_multiplexed = ( + max_in_flight_multiplexed + if max_in_flight_multiplexed is not None + else self._pool_connections * 200 + ) disabled_svn = set() @@ -454,7 +460,7 @@ def build_response( # Add new cookies from the server. extract_cookies_to_jar(response.cookies, req, resp) else: - self._promises.append(response) + self._promises[resp.uid] = response # Give the Response some context. response.request = req @@ -605,6 +611,10 @@ def send( and request.method is not None ), "Tried to send a non-initialized PreparedRequest" + # We enforce a limit to avoid burning out our connection pool. + if multiplexed and len(self._promises) >= self._max_in_flight_multiplexed: + self.gather() + try: conn = self.get_connection(request.url, proxies) except LocationValueError as e: @@ -763,6 +773,8 @@ def _future_handler(self, response: Response, low_resp: BaseHTTPResponse) -> Non for k, v in response._promise._parameters.items() if k.startswith("niquests_") } + + response_promise = response._promise del response._promise if allow_redirects: @@ -827,7 +839,7 @@ def on_post_connection(conn_info: ConnectionInfo) -> None: for k, v in promise_ctx_backup.items(): next_promise._promise.set_parameter(k, v) - self._promises.remove(response) + del self._promises[response_promise.uid] return else: @@ -889,7 +901,7 @@ def on_post_connection(conn_info: ConnectionInfo) -> None: if not stream: response.content - self._promises.remove(response) + del self._promises[response_promise.uid] def gather(self, *responses: Response) -> None: if not self._promises: @@ -898,15 +910,22 @@ def gather(self, *responses: Response) -> None: # Either we did not have a list of promises to fulfill... if not responses: while True: - response = None low_resp = self.poolmanager.get_response() if low_resp is None: break - for response in self._promises: - if low_resp.is_from_promise(response._promise): - break + assert ( + low_resp._fp is not None + and hasattr(low_resp._fp, "from_promise") + and low_resp._fp.from_promise is not None + ) + + response = ( + self._promises[low_resp._fp.from_promise.uid] + if low_resp._fp.from_promise.uid in self._promises + else None + ) if response is None: raise MultiplexingError(