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

Better document websocket::stream::async_close and teardown relationship #2730

Open
ecorm opened this issue Aug 28, 2023 · 14 comments
Open

Better document websocket::stream::async_close and teardown relationship #2730

ecorm opened this issue Aug 28, 2023 · 14 comments
Assignees
Labels
Doc A documentation specific issue

Comments

@ecorm
Copy link

ecorm commented Aug 28, 2023

Version of Beast: Boost 1.83.0

The documentation for websocket::stream::async_close does not specify what happens to the underlying TCP socket in both success and error paths (same goes for websocket::stream::close).

I'm guessing the teardown customization point has something to do with this, but there is no mention of it in the async_close documentation.

In particular, I need to know if the underlying TCP socket is closed when websocket::stream::async_close completes either successfully or with an error.

I know that I can do if (websocket_.next_layer().is_closed()) in the completion handler to find out at runtime, but I'd still like to know the behavior.

ADDENDUM: websocket::stream::async_close also does not document whether is_open() == false is a postcondition whether or not the operation succeeds. I would expect is_open() == false to always be a postcondition whether or not it succeeds.

@ecorm
Copy link
Author

ecorm commented Aug 29, 2023

I have a question related to this. When websocket::stream::async_close fails, will read operations receive an error (so that the application knows to end)?

@klemens-morgenstern
Copy link
Collaborator

What happens to the tcp socket depends on the other side. I think !is_open() is not a post-condition.

It sends the close frame to the other side, so the server might decide to just drop the connection at this point without a response (unlikely, but possible) in which case you'd get an error from the tcp layer. This would also apply to your second question.

But ideally, you'd send the close-frame to the server, which then initialized an ordered teardown, then tears down the ssl layer (which google usually skips) and then closes the socket.

@ecorm
Copy link
Author

ecorm commented Aug 29, 2023

I have refactored my server logic to not depend on websocket::stream::is_open.

I now also destroy the websocket::stream upon completion of websocket::stream::async_close (regardless of success/failure), so that should take care of closing the underlying TCP socket.

@ecorm
Copy link
Author

ecorm commented Aug 29, 2023

I now also destroy the websocket::stream upon completion of websocket::stream::async_close (regardless of success/failure), so that should take care of closing the underlying TCP socket.

Oops, I think that would violate this note from the websocket::stream destructor documentation:

A stream object must not be destroyed while there are pending asynchronous operations associated with it.

...as the websocket::async_read operation might still be pending.

The Asio sockets are more robust in this respect, as calling close or the destructor cancels all pending asynchronous operations.

@ecorm ecorm changed the title Postconditions of websocket::stream::async_close are not documented Better document websocket::stream::async_close and teardown relationship Jan 12, 2024
@ecorm
Copy link
Author

ecorm commented Jan 12, 2024

As requested by the author here, I'm bumping this issue so that the relationship between websocket::stream::async_close and teardown is better documented:

  • There's no mention of "teardown" operations in the websocket::stream::close documentation, so one needs to have a least glanced at the entire documentation to be aware that his mechanism exists, and remember that it exists. I have 1485 other things I have to constantly keep in my head, so it's easy for me to forget that websocket::stream::close performs a customizable teardown operation. 😁 Just a note with a link to the Teardown page in the websocket::stream::close documentation should suffice.
  • It's not clear if the teardown operation is completed or is kicked off by time the websocket::stream::async_close handler is invoked.
  • The Teardown documentation page does not describe what the default teardown operation actually does. It sorta implies that the default teardown is per RFC6455 section 7.1.1., but it would be better if this were explicitly stated.
  • It's not clear what state the underlying TCP socket is in if the websocket::stream::close operation fails. With Asio, ip::tcp::socket::close is a no-fail operation and I can count on the socket being actually closed from my perspective.

@xim
Copy link

xim commented Dec 12, 2024

I've also been confused by this, but wrote some code that appeared to work well for a few years. But then beast::ssl_stream was deprecated and it started crashing in some cases. At least, in new code, it looks like closing a websocket::stream will close the socket and everything «just works» if you're using ssl or tcp as in the examples? =)

@ashtum
Copy link
Collaborator

ashtum commented Dec 12, 2024

But then beast::ssl_stream was deprecated and it started crashing in some cases. At least, in new code.

The new changes shouldn't affect this behavior. Did you make any other changes to your code? In what scenario does your code crash? Could you provide a minimal reproducible example?

@xim
Copy link

xim commented Dec 13, 2024

This snippet is the code that triggers the crash, at least in some scenarios

It throws boost::asio::execution::bad_executor while handling the async_shutdown it appears.

It appears that we might be able to just drop all this extra shutdown and close stuff?

void WebsocketClient::on_close_frame_sent(boost::system::error_code ec)
{
    if (ec) {
        // We could not send a close frame => the connection is not
        // responsive => there is not going to be a clean teardown.
        boost::beast::get_lowest_layer(*stream_).close();
        return;
    }

    auto & ssl = stream_->next_layer();
    ssl.next_layer().expires_at(timeout_at_);
    ssl.async_shutdown(
        [this, self = shared_from_this()](const boost::system::error_code & ec) {
            boost::beast::get_lowest_layer(*stream_).close();
        });
}

@ashtum
Copy link
Collaborator

ashtum commented Dec 13, 2024

It throws boost::asio::execution::bad_executor while handling the async_shutdown it appears.

This is probably due to this bug: boostorg/beast#2925. It's fixed in Boost 1.87.0.

It appears that we might be able to just drop all this extra shutdown and close stuff?

void WebsocketClient::on_close_frame_sent(boost::system::error_code ec)
{
if (ec) {
// We could not send a close frame => the connection is not
// responsive => there is not going to be a clean teardown.
boost::beast::get_lowest_layer(*stream_).close();
return;
}

auto & ssl = stream_->next_layer();
ssl.next_layer().expires_at(timeout_at_);
ssl.async_shutdown(
    [this, self = shared_from_this()](const boost::system::error_code & ec) {
        boost::beast::get_lowest_layer(*stream_).close();
    });

}

You only need to initiate a websocket::stream::async_close. It handles everything for you.

By the way, this is only necessary if you are the one initiating the close operation. If the peer initiates the close, async_close will complete with websocket::error::closed, and no further action is needed.

@xim
Copy link

xim commented Dec 13, 2024

Is that only true if you use a boost::beast::ip::tcp::socket or boost::asio::ssl::stream? Is it true for boost::asio::tcp_stream as well? What about boost::asio::generic::stream_protocol::socket?

I have tried to read the documentation mentioned by @ecorm and agree that the documentation should be clear about this stuff. =)

@ashtum
Copy link
Collaborator

ashtum commented Dec 13, 2024

Is that only true if you use a boost::beast::ip::tcp::socket or boost::asio::ssl::stream? Is it true for boost::asio::tcp_stream as well? What about boost::asio::generic::stream_protocol::socket?

Sorry, which part are you referring to?

@xim
Copy link

xim commented Dec 13, 2024

Sorry, which part are you referring to?

The part here:

You only need to initiate a websocket::stream::async_close. It handles everything for you.

The async_close / teardown customization stuff doesn't make that perfectly clear.

@ashtum
Copy link
Collaborator

ashtum commented Dec 13, 2024

The part here:

You only need to initiate a websocket::stream::async_close. It handles everything for you.

Yes, this is independent of the type of underlying stream you use with websocket::stream.

websocket::async_teardown is a customization point that [async_close](websocket::stream::async_close) internally uses to close the connection. For instance, when working with asio::ssl::stream, it invokes asio::ssl::stream::async_shutdown to perform a graceful TLS shutdown. You only need to implement this function if you use a custom stream type. (It already provides a generic implementation for asio::basic_stream_socket, so it should work seamlessly with all asio stream types.)

@xim
Copy link

xim commented Dec 13, 2024

That's great news. It would be great if the documentation could be equally clear =)

@ashtum ashtum added the Doc A documentation specific issue label Dec 13, 2024
@ashtum ashtum self-assigned this Dec 13, 2024
@ashtum ashtum added this to the Boost 1.88 beta milestone Dec 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Doc A documentation specific issue
Projects
None yet
Development

No branches or pull requests

4 participants