Skip to content

Commit 717da48

Browse files
Honor max concurrent streams (#89)
* Use wait_closed with asyncio, with socket unwrapping workaround. * Fix for Python 3.6, and comments * Add type: ignore for Python 3.6 * Honor MAX_CONCURRENT_STREAMS * Drop erronous commit * Don't release stream concurrency semaphore until *after* network closing the stream * Don't use bare except
1 parent eddcc69 commit 717da48

File tree

2 files changed

+64
-26
lines changed

2 files changed

+64
-26
lines changed

httpcore/_async/http2.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from h2.exceptions import NoAvailableStreamIDError
99
from h2.settings import SettingCodes, Settings
1010

11-
from .._backends.auto import AsyncLock, AsyncSocketStream, AutoBackend
12-
from .._exceptions import ProtocolError
11+
from .._backends.auto import AsyncLock, AsyncSemaphore, AsyncSocketStream, AutoBackend
12+
from .._exceptions import PoolTimeout, ProtocolError
1313
from .._types import URL, Headers, TimeoutDict
1414
from .._utils import get_logger
1515
from .base import (
@@ -67,6 +67,17 @@ def read_lock(self) -> AsyncLock:
6767
self._read_lock = self.backend.create_lock()
6868
return self._read_lock
6969

70+
@property
71+
def max_streams_semaphore(self) -> AsyncSemaphore:
72+
# We do this lazily, to make sure backend autodetection always
73+
# runs within an async context.
74+
if not hasattr(self, "_max_streams_semaphore"):
75+
max_streams = self.h2_state.remote_settings.max_concurrent_streams
76+
self._max_streams_semaphore = self.backend.create_semaphore(
77+
max_streams, exc_class=PoolTimeout
78+
)
79+
return self._max_streams_semaphore
80+
7081
async def start_tls(self, hostname: bytes, timeout: TimeoutDict = None) -> None:
7182
pass
7283

@@ -265,16 +276,21 @@ async def request(
265276
b"content-length" in seen_headers or b"transfer-encoding" in seen_headers
266277
)
267278

268-
await self.send_headers(method, url, headers, has_body, timeout)
269-
if has_body:
270-
await self.send_body(stream, timeout)
271-
272-
# Receive the response.
273-
status_code, headers = await self.receive_response(timeout)
274-
reason_phrase = get_reason_phrase(status_code)
275-
stream = AsyncByteStream(
276-
aiterator=self.body_iter(timeout), aclose_func=self._response_closed
277-
)
279+
await self.connection.max_streams_semaphore.acquire()
280+
try:
281+
await self.send_headers(method, url, headers, has_body, timeout)
282+
if has_body:
283+
await self.send_body(stream, timeout)
284+
285+
# Receive the response.
286+
status_code, headers = await self.receive_response(timeout)
287+
reason_phrase = get_reason_phrase(status_code)
288+
stream = AsyncByteStream(
289+
aiterator=self.body_iter(timeout), aclose_func=self._response_closed
290+
)
291+
except Exception:
292+
self.connection.max_streams_semaphore.release()
293+
raise
278294

279295
return (b"HTTP/2", status_code, reason_phrase, headers, stream)
280296

@@ -346,4 +362,7 @@ async def body_iter(self, timeout: TimeoutDict) -> AsyncIterator[bytes]:
346362
break
347363

348364
async def _response_closed(self) -> None:
349-
await self.connection.close_stream(self.stream_id)
365+
try:
366+
await self.connection.close_stream(self.stream_id)
367+
finally:
368+
self.connection.max_streams_semaphore.release()

httpcore/_sync/http2.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from h2.exceptions import NoAvailableStreamIDError
99
from h2.settings import SettingCodes, Settings
1010

11-
from .._backends.auto import SyncLock, SyncSocketStream, SyncBackend
12-
from .._exceptions import ProtocolError
11+
from .._backends.auto import SyncLock, SyncSemaphore, SyncSocketStream, SyncBackend
12+
from .._exceptions import PoolTimeout, ProtocolError
1313
from .._types import URL, Headers, TimeoutDict
1414
from .._utils import get_logger
1515
from .base import (
@@ -67,6 +67,17 @@ def read_lock(self) -> SyncLock:
6767
self._read_lock = self.backend.create_lock()
6868
return self._read_lock
6969

70+
@property
71+
def max_streams_semaphore(self) -> SyncSemaphore:
72+
# We do this lazily, to make sure backend autodetection always
73+
# runs within an async context.
74+
if not hasattr(self, "_max_streams_semaphore"):
75+
max_streams = self.h2_state.remote_settings.max_concurrent_streams
76+
self._max_streams_semaphore = self.backend.create_semaphore(
77+
max_streams, exc_class=PoolTimeout
78+
)
79+
return self._max_streams_semaphore
80+
7081
def start_tls(self, hostname: bytes, timeout: TimeoutDict = None) -> None:
7182
pass
7283

@@ -265,16 +276,21 @@ def request(
265276
b"content-length" in seen_headers or b"transfer-encoding" in seen_headers
266277
)
267278

268-
self.send_headers(method, url, headers, has_body, timeout)
269-
if has_body:
270-
self.send_body(stream, timeout)
271-
272-
# Receive the response.
273-
status_code, headers = self.receive_response(timeout)
274-
reason_phrase = get_reason_phrase(status_code)
275-
stream = SyncByteStream(
276-
iterator=self.body_iter(timeout), close_func=self._response_closed
277-
)
279+
self.connection.max_streams_semaphore.acquire()
280+
try:
281+
self.send_headers(method, url, headers, has_body, timeout)
282+
if has_body:
283+
self.send_body(stream, timeout)
284+
285+
# Receive the response.
286+
status_code, headers = self.receive_response(timeout)
287+
reason_phrase = get_reason_phrase(status_code)
288+
stream = SyncByteStream(
289+
iterator=self.body_iter(timeout), close_func=self._response_closed
290+
)
291+
except Exception:
292+
self.connection.max_streams_semaphore.release()
293+
raise
278294

279295
return (b"HTTP/2", status_code, reason_phrase, headers, stream)
280296

@@ -346,4 +362,7 @@ def body_iter(self, timeout: TimeoutDict) -> Iterator[bytes]:
346362
break
347363

348364
def _response_closed(self) -> None:
349-
self.connection.close_stream(self.stream_id)
365+
try:
366+
self.connection.close_stream(self.stream_id)
367+
finally:
368+
self.connection.max_streams_semaphore.release()

0 commit comments

Comments
 (0)