Skip to content

Commit d56a32a

Browse files
committed
perf: check the bound event loop inline and call the guard only on a mismatch
1 parent 4874634 commit d56a32a

4 files changed

Lines changed: 14 additions & 19 deletions

File tree

‎src/httpware/middleware/resilience/_event_loop_guard.py‎

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,17 @@ def check_event_loop(
1111
loop_lock: threading.Lock,
1212
message_template: str,
1313
) -> None:
14-
"""Bind the caller to the first event loop that calls it.
14+
"""Bind the caller to the running loop if it has none yet, else raise RuntimeError.
1515
16-
Raises RuntimeError on a later call from a different loop. `get_loop`/`set_loop`
17-
read and write the caller's cached-loop attribute. The inner check re-reads via
18-
`get_loop()` rather than reusing the outer snapshot, so a thread that loses the
19-
race to acquire `loop_lock` still sees whichever loop the winner just bound
20-
(double-checked locking) — the outer unlocked read handles the common
21-
already-bound case without lock overhead.
16+
The slow path of the caller's unlocked `self._loop is asyncio.get_running_loop()` check.
17+
`get_loop`/`set_loop` read and write the caller's cached-loop attribute; the read under
18+
`loop_lock` sees a loop that another thread bound after the caller's unlocked read.
2219
"""
2320
current = asyncio.get_running_loop()
24-
cached = get_loop()
25-
if cached is current:
26-
return
27-
if cached is not None:
28-
raise RuntimeError(message_template.format(first=cached, current=current))
2921
with loop_lock:
3022
cached = get_loop()
3123
if cached is None:
3224
set_loop(current)
33-
elif cached is not current:
34-
raise RuntimeError(message_template.format(first=cached, current=current))
25+
return
26+
if cached is not current:
27+
raise RuntimeError(message_template.format(first=cached, current=current))

‎src/httpware/middleware/resilience/bulkhead.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ def _check_loop(self) -> None:
110110

111111
async def __call__(self, request: httpx2.Request, next: AsyncNext) -> httpx2.Response: # noqa: A002
112112
"""Acquire a slot (bounded by acquire_timeout), invoke next, release."""
113-
self._check_loop()
113+
if self._loop is not asyncio.get_running_loop():
114+
self._check_loop()
114115
try:
115116
if self._acquire_timeout is None or not self._sem.locked():
116117
await self._sem.acquire()

‎src/httpware/middleware/resilience/circuit_breaker.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,8 @@ def state(self) -> CircuitState:
564564

565565
async def __call__(self, request: httpx2.Request, next: AsyncNext) -> httpx2.Response: # noqa: A002
566566
"""Admit, forward, then record the outcome. Fast-fail when the circuit is not closed."""
567-
self._check_loop()
567+
if self._loop is not asyncio.get_running_loop():
568+
self._check_loop()
568569
return await _call_async(self._state, request, next)
569570

570571

@@ -659,7 +660,8 @@ def _check_loop(self) -> None:
659660

660661
async def __call__(self, request: httpx2.Request, next: AsyncNext) -> httpx2.Response: # noqa: A002
661662
"""Pick the request's circuit, then admit, forward and record the outcome on it."""
662-
self._check_loop()
663+
if self._loop is not asyncio.get_running_loop():
664+
self._check_loop()
663665
return await _call_async(self._states.get(request), request, next)
664666

665667

‎tests/test_event_loop_guard.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88

99
async def test_raises_when_another_loop_binds_between_unlocked_and_locked_read() -> None:
1010
other_loop = asyncio.new_event_loop()
11-
reads = iter([None, other_loop])
1211
bound: list[asyncio.AbstractEventLoop] = []
1312
try:
1413
with pytest.raises(RuntimeError, match="bound to"):
1514
check_event_loop(
16-
lambda: next(reads),
15+
lambda: other_loop,
1716
bound.append,
1817
threading.Lock(),
1918
"bound to {first}, called from {current}",

0 commit comments

Comments
 (0)