[Fix] Serialize FanOutCommunicator queueing calls with a FIFO-fair asyncio.Lock (#30606)

This commit is contained in:
Liangsheng Yin
2026-07-09 00:04:43 -07:00
committed by GitHub
parent 9e4483d725
commit 64e2a73c80
2 changed files with 66 additions and 21 deletions
+15 -21
View File
@@ -2,8 +2,7 @@ from __future__ import annotations
import asyncio
import copy
from collections import deque
from typing import Callable, Deque, Generic, List, Optional, TypeVar
from typing import Callable, Generic, List, Optional, TypeVar
T = TypeVar("T")
@@ -31,31 +30,26 @@ class FanOutCommunicator(Generic[T]):
self._mode = mode
self._result_event: Optional[asyncio.Event] = None
self._result_values: Optional[List[T]] = None
self._ready_queue: Deque[asyncio.Event] = deque()
self._queueing_lock = asyncio.Lock()
assert mode in ["queueing", "watching"]
async def queueing_call(self, obj: T):
ready_event = asyncio.Event()
if self._result_event is not None or len(self._ready_queue) > 0:
self._ready_queue.append(ready_event)
await ready_event.wait()
assert self._result_event is None
assert self._result_values is None
# asyncio.Lock is FIFO-fair: a new caller cannot acquire while earlier
# callers are still waiting, so requests are strictly serialized in
# arrival order. It also releases on exception/cancellation, so a
# failed caller never blocks the callers queued behind it.
async with self._queueing_lock:
if obj is not None:
self._send(obj)
if obj is not None:
self._send(obj)
self._result_event = asyncio.Event()
self._result_values = []
await self._result_event.wait()
result_values = self._result_values
self._result_event = self._result_values = None
self._result_event = asyncio.Event()
self._result_values = []
await self._result_event.wait()
result_values = self._result_values
self._result_event = self._result_values = None
if len(self._ready_queue) > 0:
self._ready_queue.popleft().set()
return result_values
return result_values
async def watching_call(self, obj):
if self._result_event is None: