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

Fix bug in RequestStream #271

Draft
wants to merge 28 commits into
base: master
Choose a base branch
from
Draft

Conversation

Ruben2424
Copy link
Contributor

See #270

@Ruben2424
Copy link
Contributor Author

Connection Errors in RequestStream now also close the connection. And the tests are fixed.
The current Error types are a bit confusing. I will see if I can fix this. Maybe we can make this type of error impossible by forcing to close connection in order to create the user facing error struct.
IMO it would also be good to make it clear to the user if the occurred locally or by the peer.
After this PR is merged I think we could make a new release.

…d replacing it with Atomics or OnceLock structures for each different thing
…started but no headers sent.

This is the fist step to fix this with a test. But the test is not perfect yet
@Ruben2424
Copy link
Contributor Author

@seanmonstar i noticed another Problem. The async accept method for the server implementation, waits for headers in the first bidirectional stream it receives:

#[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
pub async fn accept(
&mut self,
) -> Result<Option<(Request<()>, RequestStream<C::BidiStream, B>)>, Error> {
// Accept the incoming stream
let mut stream = match poll_fn(|cx| self.poll_accept_request_stream(cx)).await {
Ok(Some(s)) => FrameStream::new(BufRecvStream::new(s)),
Ok(None) => {
// We always send a last GoAway frame to the client, so it knows which was the last
// non-rejected request.
self.shutdown(0).await?;
return Ok(None);
}
Err(err) => {
match err.inner.kind {
crate::error::Kind::Closed => return Ok(None),
crate::error::Kind::Application {
code,
reason,
level: ErrorLevel::ConnectionError,
} => return Err(self.inner.close(code, reason.unwrap_or_default())),
_ => return Err(err),
};
}
};
let frame = poll_fn(|cx| stream.poll_next(cx)).await;
let req = self.accept_with_frame(stream, frame)?;
if let Some(req) = req {
Ok(Some(req.resolve().await?))
} else {
Ok(None)
}
}

This line waits for a bidirectional stream and also drives the connection forward by listening to the control-stream and with the changes from this PR also gets notified over mpsc when a connection-error in a different request-stream occurs.

let mut stream = match poll_fn(|cx| self.poll_accept_request_stream(cx)).await {

This lines from the accept method waits for headers in the accepted bidirectional stream.

let frame = poll_fn(|cx| stream.poll_next(cx)).await;
let req = self.accept_with_frame(stream, frame)?;

When a client starts a bidirectional stream but sends no headers, the accept method of this connection will block, not receiving any other requests or any data on the control-stream.

This also introduces some kind of head of line blocking, because the server cannot process any other request if the first one is slower.

I see two possible solutions:

  1. change the accept functionality to poll for new bidirectional streams and also to keep track of all bidirectional streams which did not receive headers yet. With this a few async functions will probably have to become poll functions.
  2. Introduce a new struct, which holds the bidirectional stream. This new type will be returned by the accept method. The new type has a public function to await the request headers and return the Request with the stream. As the accept method does at the moment.

I would prefer the second option, because this is a low level crate and with this the user has more control. Also the number of maximal concurrent steams is managed by the quic layer. So no need to hold this information in h3.

https://www.rfc-editor.org/rfc/rfc9114#name-streams

In contrast to HTTP/2, stream concurrency in HTTP/3 is managed by QUIC. QUIC considers a stream closed when all data has been received and sent data has been acknowledged by the peer. HTTP/2 considers a stream closed when the frame containing the END_STREAM bit has been committed to the transport. As a result, the stream for an equivalent exchange could remain "active" for a longer period of time. HTTP/3 servers might choose to permit a larger number of concurrent client-initiated bidirectional streams to achieve equivalent concurrency to HTTP/2, depending on the expected usage patterns.

What do you think?

@seanmonstar
Copy link
Member

I think it'd probably be best to tackle that in a follow-up PR. Good work!

@Ruben2424
Copy link
Contributor Author

I can do it in a "follow-up" PR. But i probably will do the "follow-up" first because this PR will get easier with that problem fixed.

@Ruben2424
Copy link
Contributor Author

I think it'd probably be best to tackle that in a follow-up PR. Good work!

I noticed, that I need to address this here, because I need changes already made in this PR. And to continue this PR I need this fixed.
I hope it will not become to difficult to review.

@seanmonstar
Copy link
Member

No problem, let me know when you think it's ready and I'll give it a review!

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