File tree Expand file tree Collapse file tree
src/httpware/middleware/resilience Expand file tree Collapse file tree Original file line number Diff line number Diff 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 ))
Original file line number Diff line number Diff 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 ()
Original file line number Diff line number Diff 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
Original file line number Diff line number Diff line change 88
99async 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}" ,
You can’t perform that action at this time.
0 commit comments