Skip to content

Feat/tcp connecotr hardening#179

Open
lxsaah wants to merge 10 commits into
mainfrom
feat/tcp-connecotr-hardening
Open

Feat/tcp connecotr hardening#179
lxsaah wants to merge 10 commits into
mainfrom
feat/tcp-connecotr-hardening

Conversation

@lxsaah

@lxsaah lxsaah commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Changes

  • Yield on synchronous accept() errors. embassy-net's TcpSocket::accept() can fail synchronously (a port-0 local_endpoint rejected as InvalidPort), and the Err arms of serve_socket_slot and the Listener<1> compat path had no await point — a tight loop with zero yields that starves the cooperative executor. Both now embassy_futures::yield_now().await, degrading a static misconfig to a debuggable warn-loop. New optional embassy-futures dep.
  • Runtime smoke test (_test-embassy-loopback). The socket pool is welded to a concrete embassy_net::tcp::TcpSocket, so its recycling and waker handoff were review-only. Now exercised over two real embassy-net stacks wired by an in-memory driver-channel crossover: recycle → re-accept, concurrent accept slots, and dialer redial after a failed connect / dropped link. Uses a wall-clock host time driver (the frozen stub clock stalls the delayed-ACK timer and hangs the first flush()). Kept off embassy-runtime and out of [dev-dependencies]; wired into make check.
  • Review cleanups. Reworded the TcpSocketSlot SAFETY comment (dialer/worker/listener-owned, not just "dialer-owned") and documented the intentional double-abort() on the recycle path.
  • Docs. New aimdb-tcp-connector/CHANGELOG.md (initial) + global changelog index link.

@lxsaah lxsaah requested a review from thaodt July 12, 2026 09:43
@lxsaah lxsaah self-assigned this Jul 12, 2026
@lxsaah lxsaah marked this pull request as ready for review July 12, 2026 09:43

@thaodt thaodt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with some small changes then we're good to go.

Comment thread aimdb-tcp-connector/Cargo.toml Outdated
Comment thread aimdb-tcp-connector/tests/embassy_loopback.rs Outdated
Comment thread aimdb-tcp-connector/tests/embassy_loopback.rs Outdated
Comment thread aimdb-tcp-connector/tests/embassy_loopback.rs Outdated
test and others added 4 commits July 13, 2026 18:35
…atchdog

The loopback harness polls two non-terminating embassy-net stacks in the
background, so a transport regression or a dropped crossover packet would
leave `block_on` pending until the outer CI timeout instead of failing the
test. Race the foreground/background `select` against a 20s wall-clock
watchdog that re-arms its waker each poll (so the deadline is observed even
when the stacks would otherwise park) and panics with a clear message.

Addresses review feedback on #179.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed N=2 path

The Embassy loopback concurrency test used two unrelated `TcpListener<1>` on
different ports, so it never touched `with_buffers` construction or same-port
fan-out. Rework it to stand up one `TcpListener::<2>::with_buffers(...)` on a
single port and dial it with two clients.

Reaching the N>1 pool from an integration test required a public accept: the
`Listener` impl is `N=1`-only and the pooled path (`into_server_futures`/
`serve_socket_slot`) was private, reachable only through `TcpServer<N>` + the
AimX session engine. Add `TcpListener::<N>::accept_on(index)` and factor the
accept body — previously duplicated between the `N=1` `Listener` impl and the
server workers — into a shared `accept_on_slot`. Side benefit: `with_buffers`
can now be driven directly instead of only through `TcpServer`.

Addresses review feedback on #179.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lxsaah lxsaah requested a review from thaodt July 13, 2026 19:11
Comment thread aimdb-tcp-connector/tests/embassy_loopback.rs Outdated
Comment on lines +303 to +306
pub fn accept_on(
&self,
index: usize,
) -> impl Future<Output = TransportResult<Box<dyn Connection>>> + Send + '_ {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a checked-out slot has 2 separate tasks waiting on accept_on(index), both call poll_take, but TcpSocketSlot stores only one Option<Waker>, the 2nd registration overwrites the first, so after the socket returns only one task wakes while the other can remain pending forever.
Because this new method takes &self, safe callers can create this state, so enforce one waiter per index or support multiple waiters.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, a &self accept that can touch many slots shouldn't be public. So:

  • accept_on is now test-only, hidden behind
    #[cfg(feature = "_test-embassy-loopback")] + #[doc(hidden)].
  • Listener<1>::accept calls accept_on_slot directly and the real pooled
    path is TcpServer<N>.

That means nothing we ship can put two waiters on one slot anymore. The only
caller left is the test and it does one accept per slot.

Comment on lines +347 to +349
let mut socket = poll_fn(|cx| slot.poll_take(cx)).await;
socket.abort();
match socket.accept(local_endpoint).await {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an accept_on future is dropped after taking the socket but while TcpSocket::accept is pending - for e.g., when a timeout or shutdown select wins, the async local drops the TcpSocket instead of calling slot.put. The slot remains None, so every later accept on that index waits forever, you may want to retain the socket in a drop guard or otherwise return it on cancellation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I reworked it so that a SlotReturn guard holds the socket while accept() runs. On success it hands the socket off. If the accept is dropped first, the guard puts the socket back in the slot instead of losing it. Added cancelled_accept_recycles_socket to cover it.

@lxsaah

lxsaah commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Your are right, but I think this change will lead to a broader api change. Let me check and come back!

@lxsaah

lxsaah commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Rather than reshaping the public API, I opted to feature-gate accept_on so it remains a test-only hook. The concurrent pooled-accept path that matters in production is already handled a layer up by TcpServer<N> , so there's no need to expose a &self multi-slot accept on the connector itself.

I also kept the transport-level fan-out test rather than replacing it with a full TcpServer<N> test. Driving the real worker path would pull the AimX session engine and the adapter runtime into the connector's test build and I'd prefer to keep that dependency out of this layer.

@lxsaah lxsaah requested a review from thaodt July 14, 2026 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants