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

Honor max concurrent streams #89

Merged
merged 8 commits into from
May 14, 2020
Merged

Conversation

tomchristie
Copy link
Member

Closes #85

Work based off #38

Copy link
Contributor

@yeraydiazdiaz yeraydiazdiaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Left a couple of specific comments in the code.

Checking out the branch and testing against this very simple script:

import asyncio

import httpx


async def main(url, n):
    async with httpx.AsyncClient(
        timeout=httpx.Timeout(5.0),
        http2=True,
    ) as client:
        await asyncio.gather(*[request(client, url, i) for i in range(n)])


async def request(client, url, i):
    response = await client.get(url)
    assert response.status_code == 200
    assert response.http_version == "HTTP/2"


if __name__ == "__main__":
    asyncio.run(main("https://example.org", 200))

It fails around 1/2 to 1/3 of the time with the following traceback:

Traceback (most recent call last):
  File ".dev/gather_http2_reuse.py", line 23, in <module>
    asyncio.run(main("https://example.org", 200))
  File "/Users/yeray/.pyenv/versions/3.8.2/lib/python3.8/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/Users/yeray/.pyenv/versions/3.8.2/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File ".dev/gather_http2_reuse.py", line 13, in main
    await asyncio.gather(*[request(client, url, i) for i in range(n)])
  File ".dev/gather_http2_reuse.py", line 17, in request
    response = await client.get(url)
  File "/Users/yeray/code/personal/_forks/httpx/httpx/_client.py", line 1275, in get
    return await self.request(
  File "/Users/yeray/code/personal/_forks/httpx/httpx/_client.py", line 1117, in request
    response = await self.send(
  File "/Users/yeray/code/personal/_forks/httpx/httpx/_client.py", line 1138, in send
    response = await self.send_handling_redirects(
  File "/Users/yeray/code/personal/_forks/httpx/httpx/_client.py", line 1165, in send_handling_redirects
    response = await self.send_handling_auth(
  File "/Users/yeray/code/personal/_forks/httpx/httpx/_client.py", line 1202, in send_handling_auth
    response = await self.send_single_request(request, timeout)
  File "/Users/yeray/code/personal/_forks/httpx/httpx/_client.py", line 1234, in send_single_request
    ) = await dispatcher.request(
  File "/Users/yeray/code/personal/_forks/httpcore/httpcore/_async/connection_pool.py", line 152, in request
    response = await connection.request(
  File "/Users/yeray/code/personal/_forks/httpcore/httpcore/_async/connection.py", line 78, in request
    return await self.connection.request(method, url, headers, stream, timeout)
  File "/Users/yeray/code/personal/_forks/httpcore/httpcore/_async/http2.py", line 116, in request
    return await h2_stream.request(method, url, headers, stream, timeout)
  File "/Users/yeray/code/personal/_forks/httpcore/httpcore/_async/http2.py", line 286, in request
    status_code, headers = await self.receive_response(timeout)
  File "/Users/yeray/code/personal/_forks/httpcore/httpcore/_async/http2.py", line 338, in receive_response
    event = await self.connection.wait_for_event(self.stream_id, timeout)
  File "/Users/yeray/code/personal/_forks/httpcore/httpcore/_async/http2.py", line 192, in wait_for_event
    await self.receive_events(timeout)
  File "/Users/yeray/code/personal/_forks/httpcore/httpcore/_async/http2.py", line 206, in receive_events
    raise ProtocolError(event)
httpcore._exceptions.ProtocolError: <StreamReset stream_id:363, error_code:ErrorCodes.REFUSED_STREAM, remote_reset:True>

Not really sure why it's only sometimes though, curious to know if you're also getting it.

httpcore/_backends/asyncio.py Outdated Show resolved Hide resolved
httpcore/_async/http2.py Outdated Show resolved Hide resolved
httpcore/_async/http2.py Show resolved Hide resolved
httpcore/_async/http2.py Outdated Show resolved Hide resolved
@tomchristie
Copy link
Member Author

I’ve not seen that no - is that before or after this change, or both?

@yeraydiazdiaz
Copy link
Contributor

That happens on master as well, but usually fails with a "no more streams available" error which your change fixes.

@tomchristie
Copy link
Member Author

I've addressed the error_code:ErrorCodes.REFUSED_STREAM issue now.
The semaphore release in _response_closed needs to occur after the network close.

@tomchristie
Copy link
Member Author

Righty. I think we're there now.
Thanks so much for your review on this one - it was invaluable.

@yeraydiazdiaz
Copy link
Contributor

My pleasure! Awesome work 🚀

@tomchristie tomchristie merged commit 717da48 into master May 14, 2020
@tomchristie tomchristie deleted the honor-max-concurrent-streams branch May 14, 2020 14:24
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

Successfully merging this pull request may close these issues.

Don't exceed MAX_CONCURRENT_STREAMS when sending HTTP/2 requests
2 participants