@@ -322,8 +322,10 @@ async def resolve(
322322 shared = self ._inflight .get (key )
323323 if shared is not None :
324324 # Somebody else has already asked for this, so there is no sense in
325- # asking twice - whether or not the answer is wanted fresh
326- payload = await self ._await (* shared , event_id , path )
325+ # asking twice - whether or not the answer is wanted fresh. The one who
326+ # asked owns the request and releases it; this reader only borrows the
327+ # answer, and walking away early must not take the request with it.
328+ payload = await self ._await (* shared , event_id , path , owner = False )
327329 return self ._to_value (payload , event_id , path )
328330
329331 request_id , future = self ._enqueue (event_id , path , call , args , reply = True )
@@ -363,7 +365,7 @@ async def resolve_many(
363365 """
364366 values : list [Any ] = [None ] * len (paths )
365367 outstanding : list [
366- tuple [int , tuple [str , tuple [str , ...]], tuple [str , Future | None ]]
368+ tuple [int , tuple [str , tuple [str , ...]], tuple [str , Future | None ], bool ]
367369 ] = []
368370 ours : list [tuple [str , tuple [str , ...]]] = []
369371 for index , path in enumerate (paths ):
@@ -373,18 +375,21 @@ async def resolve_many(
373375 continue
374376 shared = self ._inflight .get (key )
375377 if shared is not None :
376- outstanding .append ((index , key , shared ))
378+ # Somebody else asked for this one and is waiting on it too; it is
379+ # theirs to release, and this reader walking away must not take it
380+ # away from under them.
381+ outstanding .append ((index , key , shared , False ))
377382 continue
378383 entry = self ._enqueue (event_id , path , False , (), True )
379384 self ._inflight [key ] = entry
380385 ours .append (key )
381- outstanding .append ((index , key , entry ))
386+ outstanding .append ((index , key , entry , True ))
382387
383388 try :
384389 payloads = await asyncio .gather (
385390 * (
386- self ._await (request_id , future , event_id , paths [index ])
387- for index , _ , (request_id , future ) in outstanding
391+ self ._await (request_id , future , event_id , paths [index ], owner )
392+ for index , _ , (request_id , future ), owner in outstanding
388393 ),
389394 # One path going wrong must not throw away the answers the client
390395 # already gave to its neighbours in the same batch.
@@ -395,7 +400,7 @@ async def resolve_many(
395400 self ._inflight .pop (key , None )
396401
397402 failure : BaseException | None = None
398- for (index , key , _ ), payload in zip (outstanding , payloads , strict = True ):
403+ for (index , key , _ , _ ), payload in zip (outstanding , payloads , strict = True ):
399404 if isinstance (payload , asyncio .CancelledError ):
400405 raise payload # nocov - nothing cancels a request which is in flight
401406 if isinstance (payload , BaseException ):
@@ -450,23 +455,42 @@ async def _await(
450455 future : Future | None ,
451456 event_id : str ,
452457 path : Sequence [str | int ],
458+ owner : bool = True ,
453459 ) -> dict [str , Any ]:
454460 if future is None : # nocov - only reply=False requests have no future
455461 return {"ok" : True , "value" : None , "error" : None , "code" : None }
462+ timeout = REACTPY_EVENT_PROXY_TIMEOUT .current
463+ if not owner :
464+ # This is somebody else's request, so nothing here is ours to give up. The
465+ # wait is shielded: walking away must not cancel the answer the reader which
466+ # asked is still waiting to collect, and a wait of our own which ran out
467+ # says so rather than striking a round trip which that reader is still due
468+ # an answer to.
469+ try :
470+ return await asyncio .wait_for (
471+ asyncio .shield (asyncio .wrap_future (future )), timeout
472+ )
473+ except (TimeoutError , FuturesTimeoutError ):
474+ msg = self ._timeout_message (event_id , path )
475+ raise EventProxyTimeout (msg ) from None
456476 try :
457- return await asyncio .wait_for (
458- asyncio .wrap_future (future ), REACTPY_EVENT_PROXY_TIMEOUT .current
459- )
477+ return await asyncio .wait_for (asyncio .wrap_future (future ), timeout )
460478 except (TimeoutError , FuturesTimeoutError ):
461479 self ._handle_timeout (request_id , event_id , path )
462480 finally :
463481 # Whatever is still recorded for this request at this point belongs to
464482 # nobody. Either the answer came in and was routed, or it went unanswered
465- # and was counted as such - or the reader itself went away, which is the
466- # one case which would otherwise leave a request waiting forever for a
467- # handler which is no longer there to be woken by it.
483+ # and was counted as such - or the reader itself went away, which is the one
484+ # case which would otherwise leave a request waiting forever for a handler
485+ # which is no longer there to be woken by it.
468486 self ._take (request_id )
469487
488+ def _timeout_message (self , event_id : str , path : Sequence [str | int ]) -> str :
489+ return (
490+ f"the client did not answer the request for { list (path )!r} of event "
491+ f"{ event_id !r} within { REACTPY_EVENT_PROXY_TIMEOUT .current } s"
492+ )
493+
470494 def _enqueue (
471495 self ,
472496 event_id : str ,
@@ -588,10 +612,7 @@ def _handle_timeout(
588612 self , request_id : str , event_id : str , path : Sequence [str | int ]
589613 ) -> NoReturn :
590614 self ._strike (request_id )
591- msg = (
592- f"the client did not answer the request for { list (path )!r} of event "
593- f"{ event_id !r} within { REACTPY_EVENT_PROXY_TIMEOUT .current } s"
594- )
615+ msg = self ._timeout_message (event_id , path )
595616 raise EventProxyTimeout (msg )
596617
597618 def _strike (self , request_id : str ) -> None :
0 commit comments