Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http2 bottleneck? #83

Closed
sacOO7 opened this issue Feb 19, 2024 · 24 comments
Closed

http2 bottleneck? #83

sacOO7 opened this issue Feb 19, 2024 · 24 comments

Comments

@sacOO7
Copy link

sacOO7 commented Feb 19, 2024

niquests_rps_sad

againsts requests

python-requests-rocking

  • Also, I'm not sure how to set pool_connections and pool_maxsize, it just doesn't work via options. I had to set it explicitly via adapter.
@Ousret
Copy link

Ousret commented Feb 19, 2024

OK! Let's try to understand what is happening.

Immediately I'd say you are not leveraging the multiplexed aspect properly (across users) and you are not evaluating the results yielded by your tests properly.

A quick test show the following results (https://rest.ably.io/time x HTTP/2):

without multiplexing: around ~30 TPS (which is reasonable given the ~30ms response time)
with multiplexing: around ~800 TPS (27 times faster)

with a single thread. So the hundred users seems useless or not configured properly.
HTTP/2 consumes more CPU resource compared to HTTP/1.1 for a single request.

This need further investigation. You will excuse me, I only have limited time to investigate this case.
Can you try to ensure the users are properly set and didn't bottleneck around a single cpu thread/socket?

Regards,

Also, I'm not sure how to set pool_connections and pool_maxsize, it just doesn't work via options. I had to set it explicitly via adapter.

I suspect you voided multiplexing by inadvertence. And if your load testing tool access the response immediately, the effect will be negated.
Do as follow: niquests.Session(pool_connections=..., pool_maxsize=...) it should work as expected on init.

@Ousret Ousret changed the title Not production ready : ( http2 bottleneck? Feb 19, 2024
@sacOO7
Copy link
Author

sacOO7 commented Feb 19, 2024

@Ousret I am not sure how are you exactly running the tests. Locust is a battle tested tool to run load testing. By default it comes with python-requests. I modified the code for python-requests to use niquests instead. You can check this file -> https://github.com/sacOO7/locust-httpx-testing/blob/niquests/niquest_user.py

@sacOO7
Copy link
Author

sacOO7 commented Feb 19, 2024

Please don't come to the conclusion that I'm not using niquests properly. It's the exact same code we are using for python-requests. I would love to use this library in production. But given test results, doesn't give me enough confidence that I can use it in production.

@sacOO7
Copy link
Author

sacOO7 commented Feb 19, 2024

Please clone the repo https://github.com/sacOO7/locust-httpx-testing/tree/niquests at niquests branch and run locust command for the same. You can assume a scenario where you are running a flask app, a new thread is created for every single incoming request and then we use niquests singleton HTTP api to communicate with ably.

@Ousret
Copy link

Ousret commented Feb 19, 2024

Good to see you are considering it as your driver. We'll make sure it fit your needs by answering your concerns.

Locust is a battle tested tool to run load testing.

Yes. But also highly tied around the synchronous limits brought by Requests.

Please don't come to the conclusion that I'm not using niquests properly.

No worries, I am just issuing an hypothesis that the load testing tool does not leverage Niquests as it should.

I am not sure how are you exactly running the tests.

Very simple piece of code, see:

import niquests
import time

if __name__ == "__main__":

    before = time.time()

    responses = []

    with niquests.Session(multiplexed=True) as s:  # turn me off/on
        while time.time() - before < 60.:
            responses.append(
                s.get("https://rest.ably.io/time")
            )

        s.gather()

    delay = time.time() - before

    print(delay)
    print(len(responses) / delay)

Please clone the repo https://github.com/sacOO7/locust-httpx-testing/tree/niquests at niquests branch and run locust command for the same. You can assume a scenario where you are running a flask app, a new thread is created for every single incoming request and then we use niquests singleton HTTP api to communicate with ably.

We will give it a shot as soon as possible.

@sacOO7
Copy link
Author

sacOO7 commented Feb 19, 2024

Sure, I will be waiting! If niquests works as good as requests for same no. of users and same configuration, we would be happy to switch to niquests. Also, try running it for http2=False

@Ousret
Copy link

Ousret commented Feb 20, 2024

So, I took some time to start the investigations.
I did took the liberty to improve the "niquests user / adapter".

Locust requires to do the following "1 req -> 1 resp" per user so that it can collect "in real time" the status and draw the main graph.

So, here is what I get with http2=False.

Capture d’écran du 2024-02-20 08-00-41

And finally using HTTP/2

Capture d’écran du 2024-02-20 07-58-13

(i) A firm 1,5k/req per second without abnormal bumps.
(ii) A solid 1,3k/req per second with slight sign of "fatigue" due to CPU burning up (HTTP/2+)

I didn't try exceeding 50 users due to my poor CPU capabilities, reaching high temp. But you get the idea.

So, now, Locust can't explore properly multiplexing, it wasn't made this way.
If I do it manually, with simple scripts, I get a firm 2,5k req/s that spike to 3k (when CPU didn't throttle).

I hope this help.
We can explore any scenario you want and if necessary bring additional optimization if we can. If you (really) need a graph with/without multiplexing using locust, we can do it, but it requires some clever engineering (and, thus, time)


Code for locust

from __future__ import annotations

import logging
import time
from contextlib import contextmanager
from typing import Generator
from urllib.parse import urlparse, urlunparse

import niquests
from locust import User
from locust.clients import absolute_http_url_regexp
from niquests import Response, Request
from niquests.auth import HTTPBasicAuth
from niquests.exceptions import MissingSchema, InvalidSchema, InvalidURL, RequestException
import urllib3

logger = logging.getLogger(__name__)

from locust.exception import LocustError, ResponseError, CatchResponseError


# Configure everything there!
SHARED_SESSION: niquests.Session = niquests.Session(
    multiplexed=True,
    pool_connections=1,
    pool_maxsize=100,
    disable_http2=True,
    disable_http3=True,
)

class LocustResponse(Response):
    def raise_for_status(self):
        if hasattr(self, "error") and self.error:
            raise self.error
        Response.raise_for_status(self)


class ResponseContextManager(LocustResponse):
    """
    A Response class that also acts as a context manager that provides the ability to manually
    control if an HTTP request should be marked as successful or a failure in Locust's statistics

    This class is a subclass of :py:class:`Response <requests.Response>` with two additional
    methods: :py:meth:`success <locust.clients.ResponseContextManager.success>` and
    :py:meth:`failure <locust.clients.ResponseContextManager.failure>`.
    """

    _manual_result: bool | Exception | None = None
    _entered = False

    def __init__(self, response, request_event, request_meta):
        # copy data from response to this object
        self.__dict__ = response.__dict__
        self._request_event = request_event
        self.request_meta = request_meta

    def __enter__(self):
        self._entered = True
        return self

    def __exit__(self, exc, value, traceback):
        # if the user has already manually marked this response as failure or success
        # we can ignore the default behaviour of letting the response code determine the outcome
        if self._manual_result is not None:
            if self._manual_result is True:
                self.request_meta["exception"] = None
            elif isinstance(self._manual_result, Exception):
                self.request_meta["exception"] = self._manual_result
            self._report_request()
            return exc is None

        if exc:
            if isinstance(value, ResponseError):
                self.request_meta["exception"] = value
                self._report_request()
            else:
                # we want other unknown exceptions to be raised
                return False
        else:
            # Since we use the Exception message when grouping failures, in order to not get
            # multiple failure entries for different URLs for the same name argument, we need
            # to temporarily override the response.url attribute
            orig_url = self.url
            self.url = self.request_meta["name"]

            try:
                self.raise_for_status()
            except niquests.exceptions.RequestException as e:
                while (
                        isinstance(
                            e,
                            (
                                    niquests.exceptions.ConnectionError,
                                    urllib3.exceptions.ProtocolError,
                                    urllib3.exceptions.MaxRetryError,
                                    urllib3.exceptions.NewConnectionError,
                            ),
                        )
                        and e.__context__
                # Not sure if the above exceptions can ever be the lowest level, but it is good to be sure
                ):
                    e = e.__context__
                self.request_meta["exception"] = e

            self._report_request()
            self.url = orig_url

        return True

    def _report_request(self, exc=None):
        self._request_event.fire(**self.request_meta)

    def success(self):
        """
        Report the response as successful

        Example::

            with self.client.get("/does/not/exist", catch_response=True) as response:
                if response.status_code == 404:
                    response.success()
        """
        if not self._entered:
            raise LocustError(
                "Tried to set status on a request that has not yet been made. Make sure you use a with-block, like this:\n\nwith self.client.request(..., catch_response=True) as response:\n    response.success()"
            )
        self._manual_result = True

    def failure(self, exc):
        """
        Report the response as a failure.

        if exc is anything other than a python exception (like a string) it will
        be wrapped inside a CatchResponseError.

        Example::

            with self.client.get("/", catch_response=True) as response:
                if response.content == b"":
                    response.failure("No data")
        """
        if not self._entered:
            raise LocustError(
                "Tried to set status on a request that has not yet been made. Make sure you use a with-block, like this:\n\nwith self.client.request(..., catch_response=True) as response:\n    response.failure(...)"
            )
        if not isinstance(exc, Exception):
            exc = CatchResponseError(exc)
        self._manual_result = exc


class HttpSession:
    """
    Class for performing web requests and holding (session-) cookies between requests (in order
    to be able to log in and out of websites). Each request is logged so that locust can display
    statistics.

    This is a slightly extended version of `python-request <http://python-requests.org>`_'s
    :py:class:`requests.Session` class and mostly this class works exactly the same. However
    the methods for making requests (get, post, delete, put, head, options, patch, request)
    can now take a *url* argument that's only the path part of the URL, in which case the host
    part of the URL will be prepended with the HttpSession.base_url which is normally inherited
    from a User class' host attribute.

    Each of the methods for making requests also takes two additional optional arguments which
    are Locust specific and doesn't exist in python-requests. These are:

    :param name: (optional) An argument that can be specified to use as label in Locust's statistics instead of the URL path.
                 This can be used to group different URL's that are requested into a single entry in Locust's statistics.
    :param catch_response: (optional) Boolean argument that, if set, can be used to make a request return a context manager
                           to work as argument to a with statement. This will allow the request to be marked as a fail based on the content of the
                           response, even if the response code is ok (2xx). The opposite also works, one can use catch_response to catch a request
                           and then mark it as successful even if the response code was not (i.e 500 or 404).
    """

    def __init__(self, base_url, request_event, user, *args, session: niquests.Session | None = None, **kwargs):
        super().__init__(*args, **kwargs)

        self.base_url = base_url
        self.request_event = request_event
        self.user = user
        self.session = session

        # User can group name, or use the group context manager to gather performance statistics under a specific name
        # This is an alternative to passing in the "name" parameter to the requests function
        self.request_name: str | None = None

        # Check for basic authentication
        parsed_url = urlparse(self.base_url)
        if parsed_url.username and parsed_url.password:
            netloc = parsed_url.hostname
            if parsed_url.port:
                netloc += ":%d" % parsed_url.port

            # remove username and password from the base_url
            self.base_url = urlunparse(
                (parsed_url.scheme, netloc, parsed_url.path, parsed_url.params, parsed_url.query, parsed_url.fragment)
            )
            # configure requests to use basic auth
            self.auth = HTTPBasicAuth(parsed_url.username, parsed_url.password)

    def _build_url(self, path):
        """prepend url with hostname unless it's already an absolute URL"""
        if absolute_http_url_regexp.match(path):
            return path
        else:
            return f"{self.base_url}{path}"

    @contextmanager
    def rename_request(self, name: str) -> Generator[None, None, None]:
        """Group requests using the "with" keyword"""

        self.request_name = name
        try:
            yield
        finally:
            self.request_name = None

    def request(self, method, url, name=None, catch_response=False, context={}, **kwargs):
        """
        Constructs and sends a :py:class:`requests.Request`.
        Returns :py:class:`requests.Response` object.

        :param method: method for the new :class:`Request` object.
        :param url: URL for the new :class:`Request` object.
        :param name: (optional) An argument that can be specified to use as label in Locust's statistics instead of the URL path.
          This can be used to group different URL's that are requested into a single entry in Locust's statistics.
        :param catch_response: (optional) Boolean argument that, if set, can be used to make a request return a context manager
          to work as argument to a with statement. This will allow the request to be marked as a fail based on the content of the
          response, even if the response code is ok (2xx). The opposite also works, one can use catch_response to catch a request
          and then mark it as successful even if the response code was not (i.e 500 or 404).
        :param params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`.
        :param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`.
        :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
        :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
        :param files: (optional) Dictionary of ``'filename': file-like-objects`` for multipart encoding upload.
        :param auth: (optional) Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth.
        :param timeout: (optional) How long in seconds to wait for the server to send data before giving up, as a float,
            or a (`connect timeout, read timeout <user/advanced.html#timeouts>`_) tuple.
        :type timeout: float or tuple
        :param allow_redirects: (optional) Set to True by default.
        :type allow_redirects: bool
        :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
        :param stream: (optional) whether to immediately download the response content. Defaults to ``False``.
        :param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
        :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
        """

        # if group name has been set and no name parameter has been passed in; set the name parameter to group_name
        if self.request_name and not name:
            name = self.request_name

        # prepend url with hostname unless it's already an absolute URL
        url = self._build_url(url)

        start_time = time.time()
        start_perf_counter = time.perf_counter()
        response = self._send_request_safe_mode(method, url, **kwargs)
        response_time = (time.perf_counter() - start_perf_counter) * 1000

        request_before_redirect = (response.history and response.history[0] or response).request
        url = request_before_redirect.url

        if not name:
            name = request_before_redirect.path_url

        if self.user:
            context = {**self.user.context(), **context}

        # store meta data that is used when reporting the request to locust's statistics
        request_meta = {
            "request_type": method,
            "response_time": response_time,
            "name": name,
            "context": context,
            "response": response,
            "exception": None,
            "start_time": start_time,
            "url": url,
        }

        # get the length of the content, but if the argument stream is set to True, we take
        # the size from the content-length header, in order to not trigger fetching of the body
        if kwargs.get("stream", False):
            request_meta["response_length"] = int(response.headers.get("content-length") or 0)
        else:
            request_meta["response_length"] = len(response.content or b"")

        if catch_response:
            return ResponseContextManager(response, request_event=self.request_event, request_meta=request_meta)
        else:
            with ResponseContextManager(response, request_event=self.request_event, request_meta=request_meta):
                pass
            return response

    def _send_request_safe_mode(self, method, url, **kwargs):
        """
        Send an HTTP request, and catch any exception that might occur due to connection problems.

        Safe mode has been removed from requests 1.x.
        """
        try:
            return self.session.request(method, url, **kwargs)
        except (MissingSchema, InvalidSchema, InvalidURL):
            raise
        except RequestException as e:
            r = LocustResponse()
            r.error = e
            r.status_code = 0  # with this status_code, content returns None
            r.request = Request(method, url).prepare()
            return r


class NiquestsUser(User):
    abstract = True
    """If abstract is True, the class is meant to be subclassed, and users will not choose this locust during a test"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.host is None:
            raise LocustError(
                "You must specify the base host. Either in the host attribute in the User class, or on the command line using the --host option."
            )

        self.client = HttpSession(
            base_url=self.host,
            request_event=self.environment.events.request,
            user=self,
            session=SHARED_SESSION,
        )

and patch the locustfile as follow

from locust import task

from niquest_user import NiquestsUser

class NiquestsTestUser(NiquestsUser):
    @task
    def fetch_ably_time(self):
        self.client.request("GET", "/time")

@sacOO7
Copy link
Author

sacOO7 commented Feb 20, 2024

Hi @Ousret thanks for the response! To make things easy, I have written a script -> https://github.com/sacOO7/locust-httpx-testing/blob/main/cli-script.py. Found bug for httpx using the same script, you can check discussion here -> encode/httpx#3100. You can replace usage of requests with the niquests and check if it works as expected.

@Ousret
Copy link

Ousret commented Feb 21, 2024

OK. I took a deeper look into the script.
An adjustment was necessary, the original script didn't wait for the threads to complete before exiting the executor context, thus killing the childs by accident. see https://gist.github.com/Ousret/314473bee7535d4999e67d7687096375

my results correlate with above.

the only surprising thing is that httpx crash randomly with random stacktraces+deadlock rarely (conn drop, keyerror, conn reset, etc..) and tried to lower the thread count also to 25, without success, http2 is hard to maintain in a multi-threaded environment it seems. +1 to niquests then..!
but also confirm that Python and multi thread are inefficient, they scale poorly. python 3.13+ will improve things I guess.

@sacOO7
Copy link
Author

sacOO7 commented Feb 21, 2024

Hi @Ousret thanks for the response! Actually, httpx doesn't use http2 by default, it's crashing for http 1.1 itself. Apparently, httpx claim, http2 is not always a good choice, when you are making requests to different endpoints.
Can you please run script for httpx again and post stacktrace/crash errors here? Meanwhile I will try out your new script 👍
I feel we are getting super close to make niquests better than other options available in the market : )

@Ousret
Copy link

Ousret commented Feb 21, 2024

Actually, httpx doesn't use http2 by default, it's crashing for http 1.1 itself.

I didn't get any crash using http 1.1 using a dummy local server (golang httpbin)
But did get a crash using a remote server getting httpx.ConnectError: [Errno 104] Connection reset by peer at random moment. I don't know why. both requests and niquests survive the test with ease.

crash http2 local and remote no matter what.

http 1 crash

File "/home/ahmed/PycharmProjects/ably-test/ably_httpx.py", line 23, in send
    client.get(URL)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 1055, in get
    return self.request(
           ^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 828, in request
    return self.send(request, auth=auth, follow_redirects=follow_redirects)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 915, in send
    response = self._send_handling_auth(
               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 943, in _send_handling_auth
    response = self._send_handling_redirects(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 980, in _send_handling_redirects
    response = self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 1016, in _send_single_request
    response = transport.handle_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 230, in handle_request
    with map_httpcore_exceptions():
  File "/home/ahmed/.pyenv/versions/3.11.2/lib/python3.11/contextlib.py", line 155, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 84, in map_httpcore_exceptions
    raise mapped_exc(message) from exc
httpx.ConnectError: [Errno 104] Connection reset by peer

Can you please run script for httpx again and post stacktrace/crash errors here?

why not, but will do it later as I have to run it several times to collect the different crash.

Apparently, httpx claim, http2 is not always a good choice

Well, yes but actually no. If http2 is implemented with multiplexing in mind, it is worthy to be considered.
And in the discovery of those crashes, I am sure, they already know about this limitation in httpx/httpcore. A quick search shows that httpx isn't as thread-safe as expected, but surely will improve over time.

I feel we are getting super close to make niquests better than other options available in the market : )

We'll make sure of it.

@Ousret
Copy link

Ousret commented Feb 22, 2024

Here are the traces you requested, for good measures, just upgraded httpx/httpcore to latest.

Traceback (most recent call last):
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 69, in map_httpcore_exceptions
    yield
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 233, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 216, in handle_request
    raise exc from None
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 196, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 185, in handle_request
    raise exc
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 148, in handle_request
    status, headers = self._receive_response(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 292, in _receive_response
    event = self._receive_stream_event(request, stream_id)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 333, in _receive_stream_event
    self._receive_events(request, stream_id)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 361, in _receive_events
    events = self._read_incoming_data(request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 435, in _read_incoming_data
    raise self._read_exception  # pragma: nocover
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 69, in map_httpcore_exceptions
    yield
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 233, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 216, in handle_request
    raise exc from None
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 196, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 185, in handle_request
    raise exc
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 148, in handle_request
    status, headers = self._receive_response(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 292, in _receive_response
    event = self._receive_stream_event(request, stream_id)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 333, in _receive_stream_event
    self._receive_events(request, stream_id)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 361, in _receive_events
    events = self._read_incoming_data(request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 435, in _read_incoming_data
    raise self._read_exception  # pragma: nocover
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 69, in map_httpcore_exceptions
    yield
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 233, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 216, in handle_request
    raise exc from None
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 196, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 185, in handle_request
    raise exc
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 148, in handle_request
    status, headers = self._receive_response(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 292, in _receive_response
    event = self._receive_stream_event(request, stream_id)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 333, in _receive_stream_event
    self._receive_events(request, stream_id)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 361, in _receive_events
    events = self._read_incoming_data(request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 435, in _read_incoming_data
    raise self._read_exception  # pragma: nocover
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 69, in map_httpcore_exceptions
    yield
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 233, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 216, in handle_request
    raise exc from None
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 196, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 185, in handle_request
    raise exc
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 148, in handle_request
    status, headers = self._receive_response(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 292, in _receive_response
    event = self._receive_stream_event(request, stream_id)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 333, in _receive_stream_event
    self._receive_events(request, stream_id)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 361, in _receive_events
    events = self._read_incoming_data(request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 435, in _read_incoming_data
    raise self._read_exception  # pragma: nocover
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 69, in map_httpcore_exceptions
    yield
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 233, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 216, in handle_request
    raise exc from None
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 196, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 185, in handle_request
    raise exc
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 148, in handle_request
    status, headers = self._receive_response(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 292, in _receive_response
    event = self._receive_stream_event(request, stream_id)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 333, in _receive_stream_event
    self._receive_events(request, stream_id)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 361, in _receive_events
    events = self._read_incoming_data(request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 435, in _read_incoming_data
    raise self._read_exception  # pragma: nocover
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 69, in map_httpcore_exceptions
    yield
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 233, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 216, in handle_request
    raise exc from None
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 196, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 185, in handle_request
    raise exc
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 148, in handle_request
    status, headers = self._receive_response(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 292, in _receive_response
    event = self._receive_stream_event(request, stream_id)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 333, in _receive_stream_event
    self._receive_events(request, stream_id)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 361, in _receive_events
    events = self._read_incoming_data(request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 435, in _read_incoming_data
    raise self._read_exception  # pragma: nocover
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 69, in map_httpcore_exceptions
    yield
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 233, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 216, in handle_request
    raise exc from None
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 196, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 185, in handle_request
    raise exc
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 148, in handle_request
    status, headers = self._receive_response(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 292, in _receive_response
    event = self._receive_stream_event(request, stream_id)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 333, in _receive_stream_event
    self._receive_events(request, stream_id)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 361, in _receive_events
    events = self._read_incoming_data(request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 435, in _read_incoming_data
    raise self._read_exception  # pragma: nocover
    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 69, in map_httpcore_exceptions
    yield
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 233, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 216, in handle_request
    raise exc from None
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 196, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 185, in handle_request
    raise exc
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 148, in handle_request
    status, headers = self._receive_response(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 292, in _receive_response
    event = self._receive_stream_event(request, stream_id)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 333, in _receive_stream_event
    self._receive_events(request, stream_id)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 361, in _receive_events
    events = self._read_incoming_data(request)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 452, in _read_incoming_data
    raise exc
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 440, in _read_incoming_data
    raise RemoteProtocolError("Server disconnected")
httpcore.RemoteProtocolError: Server disconnected

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/ahmed/PycharmProjects/ably-test/ably_httpx.py", line 40, in <module>
    for duration in task.result():
                    ^^^^^^^^^^^^^
  File "/home/ahmed/.pyenv/versions/3.11.2/lib/python3.11/concurrent/futures/_base.py", line 456, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/.pyenv/versions/3.11.2/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result
    raise self._exception
  File "/home/ahmed/.pyenv/versions/3.11.2/lib/python3.11/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/ably_httpx.py", line 23, in send
    client.get(URL)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 1054, in get
    return self.request(
           ^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 827, in request
    return self.send(request, auth=auth, follow_redirects=follow_redirects)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 914, in send
    response = self._send_handling_auth(
               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 942, in _send_handling_auth
    response = self._send_handling_redirects(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 979, in _send_handling_redirects
    response = self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 1015, in _send_single_request
    response = transport.handle_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 232, in handle_request
    with map_httpcore_exceptions():
  File "/home/ahmed/.pyenv/versions/3.11.2/lib/python3.11/contextlib.py", line 155, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 86, in map_httpcore_exceptions
    raise mapped_exc(message) from exc
httpx.RemoteProtocolError: Server disconnected
Traceback (most recent call last):
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 69, in map_httpcore_exceptions
    yield
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 233, in handle_request
    resp = self._pool.handle_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 216, in handle_request
    raise exc from None
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 196, in handle_request
    response = connection.handle_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/connection.py", line 101, in handle_request
    return self._connection.handle_request(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 185, in handle_request
    raise exc
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 148, in handle_request
    status, headers = self._receive_response(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 292, in _receive_response
    event = self._receive_stream_event(request, stream_id)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 333, in _receive_stream_event
    self._receive_events(request, stream_id)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpcore/_sync/http2.py", line 352, in _receive_events
    raise RemoteProtocolError(self._connection_terminated)
httpcore.RemoteProtocolError: <ConnectionTerminated error_code:1, last_stream_id:8775, additional_data:None>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/ahmed/PycharmProjects/ably-test/ably_httpx.py", line 40, in <module>
    for duration in task.result():
                    ^^^^^^^^^^^^^
  File "/home/ahmed/.pyenv/versions/3.11.2/lib/python3.11/concurrent/futures/_base.py", line 449, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/.pyenv/versions/3.11.2/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result
    raise self._exception
  File "/home/ahmed/.pyenv/versions/3.11.2/lib/python3.11/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/ably_httpx.py", line 23, in send
    client.get(URL)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 1054, in get
    return self.request(
           ^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 827, in request
    return self.send(request, auth=auth, follow_redirects=follow_redirects)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 914, in send
    response = self._send_handling_auth(
               ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 942, in _send_handling_auth
    response = self._send_handling_redirects(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 979, in _send_handling_redirects
    response = self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_client.py", line 1015, in _send_single_request
    response = transport.handle_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 232, in handle_request
    with map_httpcore_exceptions():
  File "/home/ahmed/.pyenv/versions/3.11.2/lib/python3.11/contextlib.py", line 155, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/home/ahmed/PycharmProjects/ably-test/.venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 86, in map_httpcore_exceptions
    raise mapped_exc(message) from exc
httpx.RemoteProtocolError: <ConnectionTerminated error_code:1, last_stream_id:8775, additional_data:None>

Well, I consider that this thread answered your initial concerns. I am not going to dig further into httpx for obvious reasons. Others exceptions seems kind of hard to reach. I have no idea about the origin of the disfunctions.

I am closing this as I don't think further things are awaited from my part.
If you need anything else, don't hesistate to ping here or open a new issue entierly.

Regards,

@Ousret Ousret closed this as completed Feb 22, 2024
@sacOO7
Copy link
Author

sacOO7 commented Feb 22, 2024

Thanks @Ousret. This is really helpful : ). I will open up a new issue if needed 👍

@Ousret
Copy link

Ousret commented Mar 4, 2024

Update: We've landed some improvements over the performances.

http/1

image

http/2

image

results:

  • http 1 from 1.5k req/s to 1.8k req/s
  • http 2 from 1.4k req/s to 1.5k req/s

with the given locust scenario. and still with smooth graphs.

@sacOO7
Copy link
Author

sacOO7 commented Mar 4, 2024

Ohkay, how did we exactly do it? Did we add some code recently for performance improvements? Good to see there is a room for improvements under load : )

@Ousret
Copy link

Ousret commented Mar 4, 2024

Re:

I knew in advance in early stage dev that some key spots needed to be optimized in both Niquests and urllib3.future.
Most of the things we could safely improve, was improved. Nothing changes for the end-users or integrators.

@sacOO7
Copy link
Author

sacOO7 commented Mar 4, 2024

Also, try using server hosted on the cloud https://rest.ably.io/time and try using https for running all tests. In case of httpx, I didn't observe any issues for local servers as such, Things go wrong when we execute tests against cloud servers.

@sacOO7
Copy link
Author

sacOO7 commented Mar 4, 2024

Okay, I just saw your latest commit. Maybe you will need something like
https://github.com/florimondmanca/httpxprof
https://medium.com/@vpcarlos97/unleash-the-power-of-profiling-python-api-requests-effectively-with-profyle-fecac800ed66
This will help you optimize part of the code that takes unnecessary time to process payloads/responses.
This was captured from encode/httpx#838

@Ousret
Copy link

Ousret commented Mar 5, 2024

try using server hosted on the cloud

worked fine without issues.

Maybe you will need something like

already the case. we've almost ran out of optimization that are "safe".
my priority is to keep backward compatibility with requests, so some improvements have to wait.
the current state should be more than enough to justify a migration from x to niquests.

@Ousret
Copy link

Ousret commented May 2, 2024

Here is a little update on this topic.
Given latest Niquests.

previously hit 1.5k req/s using http/2.

now I can hit comfortably 1.9k req/s using http/2...

image

And that, in a pure synchronous loop. Without advanced multiplexing.
If that does not nudge you into choosing Niquests as your http backend, nothing will 😄

No improvement for http/1 as we remain at a stable 1.8k req/s.
Can you believe we have a faster http/2 than http/1 ? Finally!

image

without locust, using a dead simple script (async + multiplexing) I am getting somewhere around 2.5k req/s with http/2.

@sacOO7
Copy link
Author

sacOO7 commented May 3, 2024

Thanks, good to hear we were able to optimise the library 🙂👍.
Will ask team to try it out 👍

@Ousret
Copy link

Ousret commented May 16, 2024

Were you able to do some testing? I would gladly receive any feedback from your dev team.

last time I wrote, we broke previous record, 1.9k using http2.

now..!

image

how about 2.1k? still using http2. and look at the timings graph! still straight? isn't it?

regards,

@sacOO7
Copy link
Author

sacOO7 commented May 16, 2024

Damn, great to hear on the progress made 👍
Also, niquests is rocking in terms of downloads -> https://pypistats.org/packages/niquests. You might like to add badge for the same in the README!
Sadly, we haven't made any progress on integrating niquests yet ( there is other high priority work ). But, I will surely try running locust again locally and will get back to you. Also, I will post above stats in the internal channel 👍

@Ousret
Copy link

Ousret commented May 16, 2024

Great to see you like those improvements! I understand about the priorities.
Will be waiting for your update then.

niquests is rocking in terms of downloads

we may expect a significant rise very soon. hopefully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants