Skip to content

Commit d316188

Browse files
gautam-rlReflexcursoragent
authored
Isolate long-polls and file transfers on separate HTTP/2 pools (#825)
Co-authored-by: Reflex <reflex@runloop.ai> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6b9e1dc commit d316188

8 files changed

Lines changed: 1097 additions & 60 deletions

File tree

src/runloop_api_client/_base_client.py

Lines changed: 357 additions & 54 deletions
Large diffs are not rendered by default.

src/runloop_api_client/_client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727
from ._compat import cached_property
2828
from ._version import __version__
29+
from ._constants import DEFAULT_API_POOL_SHARDS, DEFAULT_TRANSFER_POOL_SHARDS, DEFAULT_BACKGROUND_POOL_SHARDS
2930
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
3031
from ._exceptions import RunloopError, APIStatusError
3132
from ._base_client import (
@@ -96,6 +97,11 @@ def __init__(
9697
# Enables HTTP/2 multiplexing and avoids ConnectTimeout storms under high concurrency.
9798
# Set to False to create a private connection pool (old behavior).
9899
shared_http_pool: bool = True,
100+
# Sharded H2 pools by workload (API / long-polls / transfers). Each shard ≈
101+
# one H2 connection; requests round-robin per client from a random offset.
102+
api_pool_shards: int = DEFAULT_API_POOL_SHARDS,
103+
background_pool_shards: int = DEFAULT_BACKGROUND_POOL_SHARDS,
104+
transfer_pool_shards: int = DEFAULT_TRANSFER_POOL_SHARDS,
99105
# Enable or disable schema validation for data returned by the API.
100106
# When enabled an error APIResponseValidationError is raised
101107
# if the API responds with invalid data for the expected schema.
@@ -142,6 +148,9 @@ def __init__(
142148
custom_query=default_query,
143149
_strict_response_validation=_strict_response_validation,
144150
shared_http_pool=shared_http_pool,
151+
api_pool_shards=api_pool_shards,
152+
background_pool_shards=background_pool_shards,
153+
transfer_pool_shards=transfer_pool_shards,
145154
)
146155

147156
self._idempotency_header = "x-request-id"
@@ -284,6 +293,9 @@ def copy(
284293
timeout: float | Timeout | None | NotGiven = not_given,
285294
http_client: httpx.Client | None = None,
286295
shared_http_pool: bool | None = None,
296+
api_pool_shards: int | None = None,
297+
background_pool_shards: int | None = None,
298+
transfer_pool_shards: int | None = None,
287299
max_retries: int | NotGiven = not_given,
288300
default_headers: Mapping[str, str] | None = None,
289301
set_default_headers: Mapping[str, str] | None = None,
@@ -325,6 +337,13 @@ def copy(
325337
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
326338
http_client=http_client,
327339
shared_http_pool=resolved_shared,
340+
api_pool_shards=(api_pool_shards if api_pool_shards is not None else self._api_pool_shards),
341+
background_pool_shards=(
342+
background_pool_shards if background_pool_shards is not None else self._background_pool_shards
343+
),
344+
transfer_pool_shards=(
345+
transfer_pool_shards if transfer_pool_shards is not None else self._transfer_pool_shards
346+
),
328347
max_retries=max_retries if is_given(max_retries) else self.max_retries,
329348
default_headers=headers,
330349
default_query=params,
@@ -390,6 +409,11 @@ def __init__(
390409
# Enables HTTP/2 multiplexing and avoids ConnectTimeout storms under high concurrency.
391410
# Set to False to create a private connection pool (old behavior).
392411
shared_http_pool: bool = True,
412+
# Sharded H2 pools by workload (API / long-polls / transfers). Each shard ≈
413+
# one H2 connection; requests round-robin per client from a random offset.
414+
api_pool_shards: int = DEFAULT_API_POOL_SHARDS,
415+
background_pool_shards: int = DEFAULT_BACKGROUND_POOL_SHARDS,
416+
transfer_pool_shards: int = DEFAULT_TRANSFER_POOL_SHARDS,
393417
# Enable or disable schema validation for data returned by the API.
394418
# When enabled an error APIResponseValidationError is raised
395419
# if the API responds with invalid data for the expected schema.
@@ -436,6 +460,9 @@ def __init__(
436460
custom_query=default_query,
437461
_strict_response_validation=_strict_response_validation,
438462
shared_http_pool=shared_http_pool,
463+
api_pool_shards=api_pool_shards,
464+
background_pool_shards=background_pool_shards,
465+
transfer_pool_shards=transfer_pool_shards,
439466
)
440467

441468
self._idempotency_header = "x-request-id"
@@ -578,6 +605,9 @@ def copy(
578605
timeout: float | Timeout | None | NotGiven = not_given,
579606
http_client: httpx.AsyncClient | None = None,
580607
shared_http_pool: bool | None = None,
608+
api_pool_shards: int | None = None,
609+
background_pool_shards: int | None = None,
610+
transfer_pool_shards: int | None = None,
581611
max_retries: int | NotGiven = not_given,
582612
default_headers: Mapping[str, str] | None = None,
583613
set_default_headers: Mapping[str, str] | None = None,
@@ -619,6 +649,13 @@ def copy(
619649
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
620650
http_client=http_client,
621651
shared_http_pool=resolved_shared,
652+
api_pool_shards=(api_pool_shards if api_pool_shards is not None else self._api_pool_shards),
653+
background_pool_shards=(
654+
background_pool_shards if background_pool_shards is not None else self._background_pool_shards
655+
),
656+
transfer_pool_shards=(
657+
transfer_pool_shards if transfer_pool_shards is not None else self._transfer_pool_shards
658+
),
622659
max_retries=max_retries if is_given(max_retries) else self.max_retries,
623660
default_headers=headers,
624661
default_query=params,

src/runloop_api_client/_constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
DEFAULT_MAX_RETRIES = 5
1111
DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=20, max_keepalive_connections=10)
1212

13+
# Separate H2 connection pools by workload. Each "shard" is its own httpx
14+
# client / transport (≈ one H2 connection). Defaults sized for Jetty's ~128
15+
# streams/connection: enough API/wait concurrency without oversized transfer.
16+
DEFAULT_API_POOL_SHARDS = 8
17+
DEFAULT_BACKGROUND_POOL_SHARDS = 16
18+
DEFAULT_TRANSFER_POOL_SHARDS = 2
19+
1320
INITIAL_RETRY_DELAY = 1.0
1421
MAX_RETRY_DELAY = 60.0
1522

src/runloop_api_client/sdk/async_.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from .._client import DEFAULT_MAX_RETRIES, AsyncRunloop
4343
from ._helpers import detect_content_type
4444
from .async_axon import AsyncAxon
45+
from .._constants import DEFAULT_API_POOL_SHARDS, DEFAULT_TRANSFER_POOL_SHARDS, DEFAULT_BACKGROUND_POOL_SHARDS
4546
from .async_agent import AsyncAgent
4647
from .async_devbox import AsyncDevbox
4748
from .async_scorer import AsyncScorer
@@ -1329,6 +1330,9 @@ def __init__(
13291330
default_headers: Mapping[str, str] | None = None,
13301331
default_query: Mapping[str, object] | None = None,
13311332
http_client: httpx.AsyncClient | None = None,
1333+
api_pool_shards: int = DEFAULT_API_POOL_SHARDS,
1334+
background_pool_shards: int = DEFAULT_BACKGROUND_POOL_SHARDS,
1335+
transfer_pool_shards: int = DEFAULT_TRANSFER_POOL_SHARDS,
13321336
) -> None:
13331337
"""Configure the asynchronous SDK wrapper.
13341338
@@ -1346,6 +1350,12 @@ def __init__(
13461350
:type default_query: Mapping[str, object] | None, optional
13471351
:param http_client: Custom ``httpx.AsyncClient`` instance to reuse, defaults to None
13481352
:type http_client: httpx.AsyncClient | None, optional
1353+
:param api_pool_shards: H2 shards for short RPCs (round-robin), defaults to 8
1354+
:type api_pool_shards: int, optional
1355+
:param background_pool_shards: H2 shards for long-polls (round-robin), defaults to 16
1356+
:type background_pool_shards: int, optional
1357+
:param transfer_pool_shards: H2 shards for upload/download (round-robin), defaults to 2
1358+
:type transfer_pool_shards: int, optional
13491359
"""
13501360
self.api = AsyncRunloop(
13511361
bearer_token=bearer_token,
@@ -1355,6 +1365,9 @@ def __init__(
13551365
default_headers=default_headers,
13561366
default_query=default_query,
13571367
http_client=http_client,
1368+
api_pool_shards=api_pool_shards,
1369+
background_pool_shards=background_pool_shards,
1370+
transfer_pool_shards=transfer_pool_shards,
13581371
)
13591372

13601373
self.agent = AsyncAgentOps(self.api)

src/runloop_api_client/sdk/sync.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from .benchmark import Benchmark
5151
from .blueprint import Blueprint
5252
from .mcp_config import McpConfig
53+
from .._constants import DEFAULT_API_POOL_SHARDS, DEFAULT_TRANSFER_POOL_SHARDS, DEFAULT_BACKGROUND_POOL_SHARDS
5354
from .gateway_config import GatewayConfig
5455
from .network_policy import NetworkPolicy
5556
from .storage_object import StorageObject
@@ -1354,6 +1355,9 @@ def __init__(
13541355
default_headers: Mapping[str, str] | None = None,
13551356
default_query: Mapping[str, object] | None = None,
13561357
http_client: httpx.Client | None = None,
1358+
api_pool_shards: int = DEFAULT_API_POOL_SHARDS,
1359+
background_pool_shards: int = DEFAULT_BACKGROUND_POOL_SHARDS,
1360+
transfer_pool_shards: int = DEFAULT_TRANSFER_POOL_SHARDS,
13571361
) -> None:
13581362
"""Configure the synchronous SDK wrapper.
13591363
@@ -1371,6 +1375,12 @@ def __init__(
13711375
:type default_query: Mapping[str, object] | None, optional
13721376
:param http_client: Custom ``httpx.Client`` instance to reuse, defaults to None
13731377
:type http_client: httpx.Client | None, optional
1378+
:param api_pool_shards: H2 shards for short RPCs (round-robin), defaults to 8
1379+
:type api_pool_shards: int, optional
1380+
:param background_pool_shards: H2 shards for long-polls (round-robin), defaults to 16
1381+
:type background_pool_shards: int, optional
1382+
:param transfer_pool_shards: H2 shards for upload/download (round-robin), defaults to 2
1383+
:type transfer_pool_shards: int, optional
13741384
"""
13751385
self.api = Runloop(
13761386
bearer_token=bearer_token,
@@ -1380,6 +1390,9 @@ def __init__(
13801390
default_headers=default_headers,
13811391
default_query=default_query,
13821392
http_client=http_client,
1393+
api_pool_shards=api_pool_shards,
1394+
background_pool_shards=background_pool_shards,
1395+
transfer_pool_shards=transfer_pool_shards,
13831396
)
13841397

13851398
self.agent = AgentOps(self.api)

0 commit comments

Comments
 (0)