Skip to content

Commit 2e5e4d7

Browse files
committed
test(resilience): Bulkhead sharing across clients + construct-outside-loop
Pins two behaviors: a Bulkhead instantiated at module scope (outside any event loop) works correctly when used inside one, and a single Bulkhead instance passed to multiple AsyncClient instances enforces the joint cap across all of them.
1 parent 8530f7a commit 2e5e4d7

1 file changed

Lines changed: 48 additions & 0 deletions

File tree

‎tests/test_bulkhead.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,51 @@ async def test_cancellation_before_acquire_does_not_hold_slot() -> None:
258258
response = await first
259259
assert response.status_code == HTTPStatus.OK
260260
assert handler.calls == 1 # second never reached the handler
261+
262+
263+
# Constructed at module scope on purpose — pins the construct-outside-loop behavior.
264+
_MODULE_SCOPE_BULKHEAD = Bulkhead(max_concurrent=_MAX_CONCURRENT_1, acquire_timeout=None)
265+
266+
267+
async def test_construct_outside_event_loop_then_use_inside() -> None:
268+
"""Bulkhead constructed at module scope must work when used inside an event loop."""
269+
handler = _SlowHandler(delay=0.0)
270+
client = _client(handler, bulkhead=_MODULE_SCOPE_BULKHEAD)
271+
response = await client.get("https://example.test/x")
272+
assert response.status_code == HTTPStatus.OK
273+
274+
275+
async def test_shared_bulkhead_enforces_joint_cap() -> None:
276+
"""One Bulkhead shared across two AsyncClients enforces the joint cap."""
277+
# Both clients use ONE handler that tracks combined in-flight across all calls.
278+
# asyncio is single-threaded so a plain dict counter is safe between awaits.
279+
state = {"in_flight": 0, "max_in_flight": 0}
280+
281+
async def shared_handler(request: httpx2.Request) -> httpx2.Response:
282+
state["in_flight"] += 1
283+
state["max_in_flight"] = max(state["max_in_flight"], state["in_flight"])
284+
try:
285+
await asyncio.sleep(0.02)
286+
return httpx2.Response(HTTPStatus.OK, request=request)
287+
finally:
288+
state["in_flight"] -= 1
289+
290+
shared = Bulkhead(max_concurrent=_MAX_CONCURRENT_1, acquire_timeout=None)
291+
client_a = AsyncClient(
292+
httpx2_client=httpx2.AsyncClient(transport=httpx2.MockTransport(shared_handler)),
293+
middleware=[shared],
294+
)
295+
client_b = AsyncClient(
296+
httpx2_client=httpx2.AsyncClient(transport=httpx2.MockTransport(shared_handler)),
297+
middleware=[shared],
298+
)
299+
300+
await asyncio.gather(
301+
client_a.get("https://upstream-a.example.test/x"),
302+
client_a.get("https://upstream-a.example.test/y"),
303+
client_b.get("https://upstream-b.example.test/x"),
304+
client_b.get("https://upstream-b.example.test/y"),
305+
)
306+
307+
# The shared bulkhead enforces max=1 across BOTH clients combined.
308+
assert state["max_in_flight"] <= _MAX_CONCURRENT_1

0 commit comments

Comments
 (0)