Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TcpListener: drain all pending accepts #224

Merged
merged 1 commit into from
Mar 4, 2025

Conversation

teskje
Copy link
Contributor

@teskje teskje commented Feb 23, 2025

This PR makes TcpListener::accept try to accept the next connection without waiting for a notify, if the previous connection couldn't be established because the peer already went away.

The change is meant to fix a bug where a pending connection sometimes wouldn't be accepted. Consider this series of events:

  • Server runs TcpListener::accept. There are no connections pending, so it starts waiting on the notify.
  • Client 1 sends a SYN, which gets pushed to ServerSocket::deque and ServerSocket::notify is notified.
  • Client 2 sends a SYN, which gets pushed to ServerSocket::deque and ServerSocket::notify is notified.
  • Client 1 goes away.
  • Server wakes up because the notify was notified.
  • Server tries to accept the connection from client 1, which fails because client 1 went away.
  • Server starts waiting on the notify again.

We end up in a situation where the connection from client 2 is still pending but the server doesn't accept it because it's waiting for a notification that was already sent.

@mcches
Copy link
Contributor

mcches commented Feb 25, 2025

Good catch! Could you add a test?

@teskje teskje force-pushed the tcp-accept-drain-all branch from 139f6a8 to 652ed46 Compare March 1, 2025 16:28
@teskje
Copy link
Contributor Author

teskje commented Mar 1, 2025

For sure, added one now!

@teskje teskje force-pushed the tcp-accept-drain-all branch from 652ed46 to 0841c40 Compare March 2, 2025 11:56
Copy link
Contributor

@mcches mcches left a comment

Choose a reason for hiding this comment

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

Looks pretty good! Let me know what you think about the accept simplification.

let host = world.current_host_mut();
let (syn, origin) = host.tcp.accept(self.local_addr)?;
let (syn, origin) = host.tcp.accept(self.local_addr).ok_or(Retry::Wait)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we return from the World::current closure here and avoid needing the enum? accept -> None signals the wait, syn.ack.send() -> err signals the retry, and the Ok path remains the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just to check my understanding: You are suggesting that we shorten the World::current closure to already end here and then do the syn.ack.send() and testing of the result outside of it, where we can continue the loop if necessary?

That would work, but in the Ok case we're have to enter World::current again to get access to the host back, unless I'm missing something. Still sounds a bit simpler, I'll give it a try!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pushed a change that gets rid of the enum and breaks out of the loop once we know we don't need to retry anymore. Lmk what you think!

This commit makes `TcpListener::accept` try to accept the next
connection without waiting for a notify, if the previous connection
couldn't be established because the peer already went away.

The change is meant to fix a bug where a pending connection sometimes
wouldn't be accepted. Consider this series of events:

 * Server runs `TcpListener::accept`. There are no connections pending,
   so it starts waiting on the `notify`.
 * Client 1 sends a SYN, which gets pushed to `ServerSocket::deque` and
   `ServerSocket::notify` is notified.
 * Client 2 sends a SYN, which gets pushed to `ServerSocket::deque` and
   `ServerSocket::notify` is notified.
 * Client 1 goes away.
 * Server wakes up because the `notify` was notified.
 * Server tries to accept the connection from client 1, which fails
   because client 1 went away.
 * Server starts waiting on the `notify` again.

We end up in a situation where the connection from client 2 is still
pending but the server doesn't accept it because it's waiting for a
notification that was already sent.
@teskje teskje force-pushed the tcp-accept-drain-all branch from 0841c40 to 5926783 Compare March 3, 2025 16:28
@teskje teskje requested a review from mcches March 3, 2025 16:29
@mcches mcches merged commit 248b159 into tokio-rs:main Mar 4, 2025
4 checks passed
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