@@ -326,8 +326,54 @@ def test_requester_churn_fd_bounded(mworkerqueue, proc_stats):
326326 A truly unbounded per-connection leak would grow phase 2 as much or
327327 more than phase 1.
328328 """
329+ import zmq .utils .monitor
330+
329331 pid = mworkerqueue .process .pid
330- worker = mworkerqueue .worker ()
332+
333+ # ``mworkerqueue.worker()`` only issues ``connect()``; the ROUTER↔DEALER
334+ # zmq handshake and the internal DEALER's peer-list update are
335+ # asynchronous. On the first churn cycle we would otherwise race:
336+ # a REQ can complete its own handshake with the ROUTER and land a
337+ # message in the inner DEALER before the worker REP has registered
338+ # as a routable peer, so the DEALER queues the message internally
339+ # and ``_poll_recv(worker, 2000)`` in the churn loop times out.
340+ #
341+ # Wait for the worker REP's HANDSHAKE_SUCCEEDED against the proxy's
342+ # inner DEALER *before* we start the churn loop. This is a passive
343+ # readiness signal — no test traffic is injected — so it can't
344+ # leave stale probe messages queued at the DEALER or leave the REP
345+ # in "must send" state. (An earlier attempt that used an active
346+ # probe REQ→worker round-trip pileged stale ``b"probe"`` messages
347+ # onto the worker's inbound queue, which the churn loop then
348+ # mis-read as its own reply key and ended up hanging on ``m.recv()``
349+ # in the ROUTER→REQ direction.)
350+ worker = mworkerqueue .ctx .socket (zmq .REP )
351+ worker .setsockopt (zmq .LINGER , 500 )
352+ worker .setsockopt (zmq .RCVTIMEO , 5000 )
353+ worker .setsockopt (zmq .SNDTIMEO , 5000 )
354+ monitor = worker .get_monitor_socket ()
355+ worker .connect (mworkerqueue .dealer_uri )
356+ # Keep the socket alive for the fixture's teardown sweep.
357+ mworkerqueue ._sockets .append (worker ) # noqa: SLF001 — intentional
358+
359+ handshake_deadline = time .monotonic () + 15.0
360+ while time .monotonic () < handshake_deadline :
361+ if not monitor .poll (500 ):
362+ continue
363+ try :
364+ ev = zmq .utils .monitor .recv_monitor_message (monitor , zmq .NOBLOCK )
365+ except zmq .error .Again :
366+ continue
367+ if ev .get ("event" ) == zmq .Event .HANDSHAKE_SUCCEEDED :
368+ break
369+ else :
370+ monitor .close (linger = 0 )
371+ raise AssertionError (
372+ "worker REP never completed HANDSHAKE_SUCCEEDED against the "
373+ "inner DEALER within 15s (test setup race, not the FD-bound "
374+ "assertion)"
375+ )
376+ monitor .close (linger = 0 )
331377
332378 def _churn (n_cycles : int , id_prefix : str ) -> None :
333379 for i in range (n_cycles ):
0 commit comments