Skip to content

Commit 4f7aa12

Browse files
authored
Merge pull request #1147 from asmit27rai/1128
Fix: Resolve DHT regression caused by race condition in stream muxer initialization
2 parents 7199dd8 + f16fd68 commit 4f7aa12

5 files changed

Lines changed: 79 additions & 1 deletion

File tree

libp2p/abc.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,15 @@ async def close(self) -> None:
222222
223223
"""
224224

225+
@property
226+
@abstractmethod
227+
def is_established(self) -> bool:
228+
"""
229+
Check if the connection is fully established and ready for streams.
230+
231+
:return: True if the connection is established, otherwise False.
232+
"""
233+
225234
@property
226235
@abstractmethod
227236
def is_closed(self) -> bool:

libp2p/network/swarm.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,10 +1550,21 @@ async def add_conn(
15501550
logger.debug("Swarm::add_conn | starting muxed connection")
15511551
self.manager.run_task(muxed_conn.start)
15521552
await muxed_conn.event_started.wait()
1553+
logger.debug(
1554+
f"Swarm::add_conn | event_started received for peer {muxed_conn.peer_id}"
1555+
)
1556+
# Verify connection is fully established before proceeding.
1557+
# For QUIC connections, wait for the connected event.
1558+
# For other muxers (like Yamux/Mplex), check the is_established property.
15531559
# For QUIC connections, also verify connection is established
15541560
if isinstance(muxed_conn, QUICConnection):
15551561
if not muxed_conn.is_established:
15561562
await muxed_conn._connected_event.wait()
1563+
elif not muxed_conn.is_established:
1564+
logger.warning(
1565+
f"Swarm::add_conn | muxer event_started set but "
1566+
f"is_established=False for peer {muxed_conn.peer_id}"
1567+
)
15571568
logger.debug("Swarm::add_conn | starting swarm connection")
15581569
self.manager.run_task(swarm_conn.start)
15591570
await swarm_conn.event_started.wait()

libp2p/stream_muxer/mplex/mplex.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class Mplex(IMuxedConn):
7878
event_closed: trio.Event
7979
event_started: trio.Event
8080
on_close: Callable[[], Awaitable[Any]] | None
81+
_established: bool
8182

8283
def __init__(
8384
self,
@@ -109,6 +110,23 @@ def __init__(
109110
self.event_closed = trio.Event()
110111
self.event_started = trio.Event()
111112
self.on_close = on_close
113+
self._established = False
114+
115+
@property
116+
def is_established(self) -> bool:
117+
"""
118+
Check if the Mplex connection is fully established and ready for streams.
119+
120+
Returns True when:
121+
- The event_started has been set
122+
- The handle_incoming task is actively running
123+
- The connection is not shutting down
124+
"""
125+
return (
126+
self._established
127+
and self.event_started.is_set()
128+
and not self.event_shutting_down.is_set()
129+
)
112130

113131
async def start(self) -> None:
114132
await self.handle_incoming()
@@ -221,6 +239,7 @@ async def handle_incoming(self) -> None:
221239
Read a message off of the secured connection and add it to the
222240
corresponding message buffer.
223241
"""
242+
self._established = True
224243
self.event_started.set()
225244
while True:
226245
try:

libp2p/stream_muxer/yamux/yamux.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,23 @@ def __init__(
451451
self.stream_buffers: dict[int, bytearray] = {}
452452
self.stream_events: dict[int, trio.Event] = {}
453453
self._nursery: Nursery | None = None
454+
self._established: bool = False
455+
456+
@property
457+
def is_established(self) -> bool:
458+
"""
459+
Check if the Yamux connection is fully established and ready for streams.
460+
461+
Returns True when:
462+
- The event_started has been set
463+
- The handle_incoming task is actively running
464+
- The connection is not shutting down
465+
"""
466+
return (
467+
self._established
468+
and self.event_started.is_set()
469+
and not self.event_shutting_down.is_set()
470+
)
454471

455472
async def start(self) -> None:
456473
logger.debug(f"Starting Yamux for {self.peer_id}")
@@ -463,8 +480,12 @@ async def start(self) -> None:
463480
logger.debug(
464481
f"Yamux.start() starting handle_incoming task for {self.peer_id}"
465482
)
466-
nursery.start_soon(self.handle_incoming)
483+
# Use nursery.start() to ensure handle_incoming has started
484+
# before we set event_started. This prevents race conditions
485+
# where streams are opened before the muxer is ready.
486+
await nursery.start(self._handle_incoming_with_ready_signal)
467487
logger.debug(f"Yamux.start() setting event_started for {self.peer_id}")
488+
self._established = True
468489
self.event_started.set()
469490
logger.debug(
470491
f"Yamux.start() exiting for {self.peer_id}, closing new stream channel"
@@ -691,6 +712,23 @@ async def read_stream(self, stream_id: int, n: int = -1) -> bytes:
691712
# This line should never be reached, but satisfies the type checker
692713
raise MuxedStreamEOF("Unexpected end of read_stream")
693714

715+
async def _handle_incoming_with_ready_signal(
716+
self, task_status: Any = trio.TASK_STATUS_IGNORED
717+
) -> None:
718+
"""
719+
Wrapper for handle_incoming that signals when the task is ready.
720+
721+
This method uses trio's task_status to signal that the handle_incoming
722+
loop is ready to process frames. This prevents race conditions where
723+
streams are opened before the muxer is ready to handle them.
724+
"""
725+
logger.debug(
726+
f"Yamux _handle_incoming_with_ready_signal() starting for "
727+
f"peer {self.peer_id}"
728+
)
729+
task_status.started()
730+
await self.handle_incoming()
731+
694732
async def handle_incoming(self) -> None:
695733
logger.debug(f"Yamux handle_incoming() started for peer {self.peer_id}")
696734
while not self.event_shutting_down.is_set():

newsfragments/1128.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed DHT regression on v0.5.0 where streams failed to open to bootstrap peers due to a race condition in stream muxer initialization.

0 commit comments

Comments
 (0)