-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
asio::ssl::stream::async_read_once null_buffers causes busy-loop #1015
Comments
Looking at this a bit deeper - I think this is hard to solve because for these reasons:
Without a solution to 2) I don't see how anything can call the handler function in 3) with the correct number of available bytes without a buffer to do at least some of the work. I think I see two possible solutions here:
Implementing 2) on the application side might make the most sense, it's just a shame this won't help applications out there using null_buffers with asio::ssl at the moment. |
chriskohlhoff/asio#1015 `asio::ssl` has a bug where calling async_read_some with `null_buffers` isn't correctly handled, and more or less immediately invokes the provided handler with no data. This causes `amqpprox` to busy-loop whenever a TLS socket has been accepted or opened. There were two options for how to fix this: 1) Always read with a fixed size buffer, such as 32kb. This would simplify the code slightly, at the expense of needing multiple reads to handle larger than 32kb frames in non-TLS mode, even when the full frame is available to `amqpprox` in one go. 2) Ask asio::ssl to read with a very small buffer, then ask the openssl library how many bytes are available. This technique aligns with how `amqpprox`'s read loop works today. That is what is implemented here. In theory something similar could be upstreamed into `asio::ssl`. It's a little tricky though and this exact code couldn't handle the generic `MutableBufferSequence` interface - we can take some shortcuts in our code. I've done some benchmarking to check this change isn't going to regress performance noticeably. Data throughput tests indicate that this fix improves performance for TLS connections over the existing code. Still running connection throughput tests.
chriskohlhoff/asio#1015 `asio::ssl` has a bug where calling async_read_some with `null_buffers` isn't correctly handled, and more or less immediately invokes the provided handler with no data. This causes `amqpprox` to busy-loop whenever a TLS socket has been accepted or opened. There were two options for how to fix this: 1) Always read with a fixed size buffer, such as 32kb. This would simplify the code slightly, at the expense of needing multiple reads to handle larger than 32kb frames in non-TLS mode, even when the full frame is available to `amqpprox` in one go. 2) Ask asio::ssl to read with a very small buffer, then ask the openssl library how many bytes are available. This technique aligns with how `amqpprox`'s read loop works today. That is what is implemented here. In theory something similar could be upstreamed into `asio::ssl`. It's a little tricky though and this exact code couldn't handle the generic `MutableBufferSequence` interface - we can take some shortcuts in our code. I've done some benchmarking to check this change isn't going to regress performance noticeably. Data throughput tests indicate that this fix improves performance for TLS connections over the existing code. Still running connection throughput tests.
chriskohlhoff/asio#1015 `asio::ssl` has a bug where calling async_read_some with `null_buffers` isn't correctly handled, and more or less immediately invokes the provided handler with no data. This causes `amqpprox` to busy-loop whenever a TLS socket has been accepted or opened. There were two options for how to fix this: 1) Always read with a fixed size buffer, such as 32kb. This would simplify the code slightly, at the expense of needing multiple reads to handle larger than 32kb frames in non-TLS mode, even when the full frame is available to `amqpprox` in one go. 2) Ask asio::ssl to read with a very small buffer, then ask the openssl library how many bytes are available. This technique aligns with how `amqpprox`'s read loop works today. That is what is implemented here. In theory something similar could be upstreamed into `asio::ssl`. It's a little tricky though and this exact code couldn't handle the generic `MutableBufferSequence` interface - we can take some shortcuts in our code. I've done some benchmarking to check this change isn't going to regress performance noticeably. Data throughput tests indicate that this fix improves performance for TLS connections over the existing code. Still running connection throughput tests.
chriskohlhoff/asio#1015 `asio::ssl` has a bug where calling async_read_some with `null_buffers` isn't correctly handled, and more or less immediately invokes the provided handler with no data. This causes `amqpprox` to busy-loop whenever a TLS socket has been accepted or opened. There were two options for how to fix this: 1) Always read with a fixed size buffer, such as 32kb. This would simplify the code slightly, at the expense of needing multiple reads to handle larger than 32kb frames in non-TLS mode, even when the full frame is available to `amqpprox` in one go. 2) Ask asio::ssl to read with a very small buffer, then ask the openssl library how many bytes are available. This technique aligns with how `amqpprox`'s read loop works today. That is what is implemented here. In theory something similar could be upstreamed into `asio::ssl`. It's a little tricky though and this exact code couldn't handle the generic `MutableBufferSequence` interface - we can take some shortcuts in our code. I've done some benchmarking to check this change isn't going to regress performance noticeably. Data throughput tests indicate that this fix improves performance for TLS connections over the existing code. Still running connection throughput tests.
chriskohlhoff/asio#1015 `asio::ssl` has a bug where calling async_read_some with `null_buffers` isn't correctly handled, and more or less immediately invokes the provided handler with no data. This causes `amqpprox` to busy-loop whenever a TLS socket has been accepted or opened. There were two options for how to fix this: 1) Always read with a fixed size buffer, such as 32kb. This would simplify the code slightly, at the expense of needing multiple reads to handle larger than 32kb frames in non-TLS mode, even when the full frame is available to `amqpprox` in one go. 2) Ask asio::ssl to read with a very small buffer, then ask the openssl library how many bytes are available. This technique aligns with how `amqpprox`'s read loop works today. That is what is implemented here. In theory something similar could be upstreamed into `asio::ssl`. It's a little tricky though and this exact code couldn't handle the generic `MutableBufferSequence` interface - we can take some shortcuts in our code. I've done some benchmarking to check this change isn't going to regress performance noticeably. Data throughput tests indicate that this fix improves performance for TLS connections over the existing code. Still running connection throughput tests.
I'm also seeing this issue, though because of a different reason. Looking at the example, the way it was written was to basically do async_read_some(null_buffers(), ...) ... but on the next layer which was in most cases tcp and ended up in epoll. It was wrong but used to work because up to TLS 1.2 the SSL handshake had to complete before application data was sent and in between requests for HTTP/1.1 epoll seemed to work okayish (next request received X time after response and epoll notices the bytes). With TLS 1.3, however, this is no longer the case and the application data and the client certificate ended up being buffered in OpenSSL but not yet being processed. Hence epoll now gets stuck as there's no data in the socket. The reasonable solution was to use sslsocket.async_read_some(null_buffers(), ...) and now I'm seeing this busy loop. I'm going to use the same workaround that reads 1 byte but that looks really inefficient. There's an optimization involving SSL_pending but that would require me to add a ton of extra unit tests to make sure I have covered all of the corner cases (including e.g. read 0). |
I've found an issue where calling async_read_some with the null_buffers technique (idiom?) causes an application to busy loop. Worth noting there is no async_wait equivalent available here either. Below describes the steps we go through from invoking
async_read_some
to nearly immediately having the handler invoked when there's no data.A small application along the lines of this highlights the issue (sorry it doesn't compile - on my todo list):
I believe the issue is that stream::async_read_some has no null_buffers specialisation, and therefore we eventually end up down in engine::read with an empty buffer.
asio/asio/include/asio/ssl/detail/impl/engine.ipp
Lines 202 to 209 in b89fcb8
This returns
want_nothing
, instead of what I think should be something closer towant_input_and_retry
.want_nothing
causesio_op::operator()
to get here:asio/asio/include/asio/ssl/detail/io.hpp
Lines 230 to 246 in b89fcb8
Which triggers an empty read event such that
io_op
gets re-executed in the next(ish?) executor loop iteration.When we come back round to io_op
start_
is set to 0asio/asio/include/asio/ssl/detail/io.hpp
Lines 145 to 148 in b89fcb8
and then we jump into a
default:
statement nested inside of the do { } while(); loop (which it's fair to say, surprised me):asio/asio/include/asio/ssl/detail/io.hpp
Lines 255 to 263 in b89fcb8
want_
hasn't been re-written since the last time we were here, so in my case it's stillwant_nothing
.. and finally we end up invoking the user-provided handler (via read_op) just inside the
switch(want_)
with zero bytes read and no error code. Our application (as would others) then invoke async_read_some again again (and there's the busy loop).asio/asio/include/asio/ssl/detail/io.hpp
Lines 305 to 313 in b89fcb8
I found at least one other person on stackoverflow has hit this issue before, a long time ago: https://stackoverflow.com/questions/40163626/boost-asio-ssl-not-working-as-expected-when-used-with-null-buffers
I think that there is something wrong here, unfortunately I just don't quite see where the best place to fix this issue is.
Perhaps some kind of
async_wait / async_read_some(boost::asio::null_buffers)
specialisation could drop us straight into this logic (or something like it) ?asio/asio/include/asio/ssl/detail/io.hpp
Lines 157 to 189 in b89fcb8
The text was updated successfully, but these errors were encountered: