@@ -144,6 +144,12 @@ where
144144
145145struct TcpSocketSlot {
146146 socket : RefCell < Option < TcpSocket < ' static > > > ,
147+ // Single `Waker`: at most one accept ever waits on a given slot. Every shipped
148+ // accept path holds one waiter per slot by construction — the `&mut self`
149+ // `Listener` impl serializes accepts on the sole slot, and `TcpServer<N>` runs
150+ // exactly one worker per slot. The pooled per-slot accept is test-only (see
151+ // `accept_on`), and its single caller drives one accept per index. So this
152+ // waker is never clobbered.
147153 waker : RefCell < Option < Waker > > ,
148154}
149155
@@ -190,6 +196,47 @@ impl TcpSocketSlot {
190196 }
191197}
192198
199+ /// Owns a socket taken out of its slot for the duration of an `accept()`, and
200+ /// returns it to the slot if dropped before the accept succeeds. Without this,
201+ /// an accept future dropped mid-`accept` (a `select!` timeout or shutdown branch
202+ /// winning the race) would drop the socket instead of recycling it, leaving the
203+ /// slot permanently empty so every later accept on that index waits forever.
204+ /// [`SlotReturn::into_socket`] defuses it once the socket moves into a
205+ /// [`TcpConnection`] on the success path.
206+ struct SlotReturn < ' a > {
207+ slot : & ' a Arc < TcpSocketSlot > ,
208+ socket : Option < TcpSocket < ' static > > ,
209+ }
210+
211+ impl < ' a > SlotReturn < ' a > {
212+ fn new ( slot : & ' a Arc < TcpSocketSlot > , socket : TcpSocket < ' static > ) -> Self {
213+ Self {
214+ slot,
215+ socket : Some ( socket) ,
216+ }
217+ }
218+
219+ fn socket_mut ( & mut self ) -> & mut TcpSocket < ' static > {
220+ self . socket
221+ . as_mut ( )
222+ . expect ( "socket present until into_socket" )
223+ }
224+
225+ /// Take the socket back, defusing the guard so its `Drop` becomes a no-op.
226+ fn into_socket ( mut self ) -> TcpSocket < ' static > {
227+ self . socket . take ( ) . expect ( "socket taken exactly once" )
228+ }
229+ }
230+
231+ impl Drop for SlotReturn < ' _ > {
232+ fn drop ( & mut self ) {
233+ if let Some ( mut socket) = self . socket . take ( ) {
234+ socket. abort ( ) ;
235+ self . slot . put ( socket) ;
236+ }
237+ }
238+ }
239+
193240/// A reusable Embassy TCP dialer backed by one caller-owned socket.
194241///
195242/// Unlike moved-in UART peripherals, `embassy-net` TCP sockets can be reused
@@ -292,14 +339,17 @@ impl<const N: usize> TcpListener<N> {
292339 }
293340 }
294341
295- /// Accept one connection on pooled socket `index`, recycling that socket
296- /// back into its slot when the returned connection drops.
342+ /// Accept one connection on pooled socket `index`, recycling that socket back
343+ /// into its slot when the returned connection drops. **Test-only** (gated on
344+ /// `_test-embassy-loopback`): the shipped pooled path is `TcpServer<N>`, which
345+ /// runs one worker per slot; this lets the transport-level loopback test drive
346+ /// the same-port fan-out directly, without the session engine.
297347 ///
298- /// The [`Listener`] impl (`N = 1`) and the `TcpServer` workers each accept a
299- /// single slot at a time; this exposes the same per-slot accept so a caller
300- /// can keep several pooled sockets in `accept()` on one endpoint at once
301- /// (the concurrent-listener property `with_buffers` exists for). Panics if
302- /// `index >= N`.
348+ /// The slot stores a single `Waker`, so this takes `&self` on the contract
349+ /// that the caller drives **at most one accept per `index`** (the test uses
350+ /// distinct indices). Panics if `index >= N`.
351+ # [ cfg ( feature = "_test-embassy-loopback" ) ]
352+ # [ doc ( hidden ) ]
303353 pub fn accept_on (
304354 & self ,
305355 index : usize ,
@@ -333,26 +383,42 @@ impl<const N: usize> TcpListener<N> {
333383
334384impl Listener for TcpListener < 1 > {
335385 fn accept ( & mut self ) -> BoxFut < ' _ , TransportResult < Box < dyn Connection > > > {
336- Box :: pin ( self . accept_on ( 0 ) )
386+ // `&mut self` already serializes accepts on the sole slot, so the
387+ // one-waiter-per-slot invariant holds here.
388+ let slot = self . slots [ 0 ] . clone ( ) ;
389+ let local_endpoint = self . local_endpoint ;
390+ Box :: pin ( SendFutureWrapper ( async move {
391+ accept_on_slot ( & slot, local_endpoint) . await
392+ } ) )
337393 }
338394}
339395
340396/// Take the socket from `slot`, accept one inbound connection on it, and hand
341- /// back a recyclable [`TcpConnection`]. Shared by [`TcpListener::accept_on`] and
342- /// the per-slot server workers so all pooled accept paths behave identically.
397+ /// back a recyclable [`TcpConnection`]. Shared by the [`Listener`] impl, the
398+ /// per-slot server workers, and the test-only `accept_on` so all pooled accept
399+ /// paths behave identically.
343400async fn accept_on_slot (
344401 slot : & Arc < TcpSocketSlot > ,
345402 local_endpoint : IpListenEndpoint ,
346403) -> TransportResult < Box < dyn Connection > > {
347- let mut socket = poll_fn ( |cx| slot. poll_take ( cx) ) . await ;
348- socket. abort ( ) ;
349- match socket. accept ( local_endpoint) . await {
404+ let socket = poll_fn ( |cx| slot. poll_take ( cx) ) . await ;
405+ // The socket lives in this guard until it either moves into a `TcpConnection`
406+ // (success) or is returned to the slot. If the whole future is dropped while
407+ // `accept()` is still pending, the guard's `Drop` recycles the socket so the
408+ // slot is never left permanently empty.
409+ let mut guard = SlotReturn :: new ( slot, socket) ;
410+ guard. socket_mut ( ) . abort ( ) ;
411+ // Bind the result before matching so the `accept()` future's borrow of
412+ // `guard` ends here, freeing `guard` for `into_socket` / `drop` below.
413+ let accepted = guard. socket_mut ( ) . accept ( local_endpoint) . await ;
414+ match accepted {
350415 Ok ( ( ) ) => {
416+ let socket = guard. into_socket ( ) ;
351417 Ok ( Box :: new ( TcpConnection :: reusable ( socket, slot. clone ( ) ) ) as Box < dyn Connection > )
352418 }
353419 Err ( _) => {
354- socket. abort ( ) ;
355- slot . put ( socket ) ;
420+ // Dropping `guard` aborts the socket and returns it to the slot.
421+ drop ( guard ) ;
356422 // `accept()` can fail synchronously (e.g. port-0 `InvalidPort`);
357423 // without this await the caller re-enters `accept()` immediately with
358424 // no yield point, starving the executor. Yield so a misconfig
0 commit comments