44import json
55import time
66import uuid
7- import zlib
87import email
98import asyncio
109import inspect
@@ -179,7 +178,8 @@ async def aclose(self) -> None:
179178
180179# Sharded H2 bulkheads: long-polls and file transfers stay off the API control-plane
181180# connection. Each shard index maps to its own shared transport (≈ one H2 connection).
182- # Removable once httpcore respects stream capacity when opening connections.
181+ # Per-client round-robin spreads concurrent requests across shards. Removable once
182+ # httpcore respects stream capacity when opening connections.
183183_shared_sync_background_transports : dict [int , _SharedTransport ] = {}
184184_shared_sync_transfer_transports : dict [int , _SharedTransport ] = {}
185185_shared_async_background_transports : weakref .WeakKeyDictionary [
@@ -201,30 +201,6 @@ def _is_transfer_path(path: str) -> bool:
201201 return path .endswith (_TRANSFER_PATH_SUFFIXES )
202202
203203
204- def _pool_affinity_key (path : str ) -> str :
205- """Pick a stable resource id from the URL for shard routing."""
206- parts = [p for p in path .split ("/" ) if p ]
207- try :
208- if "executions" in parts :
209- idx = parts .index ("executions" )
210- if idx + 1 < len (parts ):
211- return parts [idx + 1 ]
212- if "devboxes" in parts :
213- idx = parts .index ("devboxes" )
214- if idx + 1 < len (parts ):
215- return parts [idx + 1 ]
216- except ValueError :
217- pass
218- return path
219-
220-
221- def _shard_index (key : str , shards : int ) -> int :
222- if shards <= 1 :
223- return 0
224- # crc32 is stable across processes (unlike PYTHONHASHSEED-randomized hash()).
225- return zlib .crc32 (key .encode ("utf-8" )) % shards
226-
227-
228204def _acquire_shared_sync_transport (bucket : dict [int , _SharedTransport ], shard : int ) -> _SharedTransport :
229205 with _pool_lock :
230206 existing = bucket .get (shard )
@@ -1019,6 +995,8 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
1019995 _isolate_workload_pools : bool
1020996 _background_pool_shards : int
1021997 _transfer_pool_shards : int
998+ _background_next : int
999+ _transfer_next : int
10221000 _closed : bool
10231001
10241002 def __init__ (
@@ -1076,6 +1054,8 @@ def __init__(
10761054 self ._bulkhead_lock = threading .Lock ()
10771055 self ._background_pool_shards = background_pool_shards
10781056 self ._transfer_pool_shards = transfer_pool_shards
1057+ self ._background_next = 0
1058+ self ._transfer_next = 0
10791059 # Custom http_client owns the full transport stack; don't invent sibling pools.
10801060 self ._isolate_workload_pools = http_client is None
10811061
@@ -1151,15 +1131,27 @@ def _ensure_transfer_client(self, shard: int) -> httpx.Client:
11511131 self ._transfer_clients [shard ] = client
11521132 return client
11531133
1134+ def _next_background_client (self ) -> httpx .Client :
1135+ # Select under the lock; ensure afterward so _ensure_* can take the same lock.
1136+ with self ._bulkhead_lock :
1137+ shard = self ._background_next % self ._background_pool_shards
1138+ self ._background_next += 1
1139+ return self ._ensure_background_client (shard )
1140+
1141+ def _next_transfer_client (self ) -> httpx .Client :
1142+ with self ._bulkhead_lock :
1143+ shard = self ._transfer_next % self ._transfer_pool_shards
1144+ self ._transfer_next += 1
1145+ return self ._ensure_transfer_client (shard )
1146+
11541147 def _send_client_for_request (self , request : httpx .Request ) -> httpx .Client :
11551148 if not self ._isolate_workload_pools :
11561149 return self ._client
11571150 path = request .url .path
1158- key = _pool_affinity_key (path )
11591151 if _is_background_path (path ):
1160- return self ._ensure_background_client ( _shard_index ( key , self . _background_pool_shards ) )
1152+ return self ._next_background_client ( )
11611153 if _is_transfer_path (path ):
1162- return self ._ensure_transfer_client ( _shard_index ( key , self . _transfer_pool_shards ) )
1154+ return self ._next_transfer_client ( )
11631155 return self ._client
11641156
11651157 def is_closed (self ) -> bool :
@@ -1736,6 +1728,8 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
17361728 _isolate_workload_pools : bool
17371729 _background_pool_shards : int
17381730 _transfer_pool_shards : int
1731+ _background_next : int
1732+ _transfer_next : int
17391733 _closed : bool
17401734
17411735 def __init__ (
@@ -1792,6 +1786,8 @@ def __init__(
17921786 self ._transfer_clients = {}
17931787 self ._background_pool_shards = background_pool_shards
17941788 self ._transfer_pool_shards = transfer_pool_shards
1789+ self ._background_next = 0
1790+ self ._transfer_next = 0
17951791 # Custom http_client owns the full transport stack; don't invent sibling pools.
17961792 self ._isolate_workload_pools = http_client is None
17971793
@@ -1877,15 +1873,25 @@ def _ensure_transfer_client(self, shard: int) -> httpx.AsyncClient:
18771873 self ._transfer_clients [shard ] = client
18781874 return client
18791875
1876+ def _next_background_client (self ) -> httpx .AsyncClient :
1877+ # Single-threaded event loop: counter bump needs no lock when there is no await.
1878+ shard = self ._background_next % self ._background_pool_shards
1879+ self ._background_next += 1
1880+ return self ._ensure_background_client (shard )
1881+
1882+ def _next_transfer_client (self ) -> httpx .AsyncClient :
1883+ shard = self ._transfer_next % self ._transfer_pool_shards
1884+ self ._transfer_next += 1
1885+ return self ._ensure_transfer_client (shard )
1886+
18801887 def _send_client_for_request (self , request : httpx .Request ) -> httpx .AsyncClient :
18811888 if not self ._isolate_workload_pools :
18821889 return self ._client
18831890 path = request .url .path
1884- key = _pool_affinity_key (path )
18851891 if _is_background_path (path ):
1886- return self ._ensure_background_client ( _shard_index ( key , self . _background_pool_shards ) )
1892+ return self ._next_background_client ( )
18871893 if _is_transfer_path (path ):
1888- return self ._ensure_transfer_client ( _shard_index ( key , self . _transfer_pool_shards ) )
1894+ return self ._next_transfer_client ( )
18891895 return self ._client
18901896
18911897 def is_closed (self ) -> bool :
0 commit comments