Search code examples
c++boostboost-beast

Understand the usage of timeout in beast::tcp_stream?


Reference:

https://www.boost.org/doc/libs/1_78_0/libs/beast/example/websocket/client/async/websocket_client_async.cpp https://www.boost.org/doc/libs/1_78_0/libs/beast/doc/html/beast/using_io/timeouts.html https://www.boost.org/doc/libs/1_78_0/libs/beast/doc/html/beast/ref/boost__beast__tcp_stream.html

    void on_resolve(beast::error_code ec, tcp::resolver::results_type results)
    {
        if(ec) return fail(ec, "resolve");
        // Set the timeout for the operation
        beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30));

        // Make the connection on the IP address we get from a lookup
        beast::get_lowest_layer(ws_).async_connect(
            results, beast::bind_front_handler(
                &session::on_connect, shared_from_this()));
    }

    void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type ep)
    {
        if(ec) return fail(ec, "connect");

        // Turn off the timeout on the tcp_stream, because
        // the websocket stream has its own timeout system.
        // beast::get_lowest_layer(ws_).expires_never(); // Note: do NOT call this line for this question!!!

...
        host_ += ':' + std::to_string(ep.port());
        // Perform the websocket handshake
        ws_.async_handshake(host_, "/",
            beast::bind_front_handler(&session::on_handshake, shared_from_this()));
    }

Question 1> Will the timeout of beast::tcp_stream continue to work after a previous asynchronous operation finishes on time?

For example, In above example, the timeout will expire after 30 seconds. If async_connect doesn't finish within 30 seconds, session::on_connect will receive an error::timeout as the value of ec. Let's assume the async_connect takes 10 seconds, can I assume that async_handshake needs to finish within 20(i.e. 30-10) seconds otherwise a error::timeout will be sent to session::on_handshake? I infer to this idea based on the comments within on_connect function(i.e.

Turn off the timeout on the tcp_stream

). In other words, a timeout will only be turned off after it finishes the specified expiration period or is disabled by expires_never. Is my understanding correct?

Question 2> Also I want to know what a good pattern I should use for timeout in both async_calling and async_callback functions.

When we call an async_calling operation:

void func_async_calling()
{
  // set some timeout here(i.e. XXXX seconds)
  Step 1> beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(XXXX));
  Step 2> ws_.async_operation(..., func_async_callback, )
  Step 3> beast::get_lowest_layer(ws_).expires_never();
}

When we define a async_callback handle for an asynchronous operation:

void func_async_callback()
{
  Step 1>Either call 
    // Disable the timeout for the next logical operation.
    beast::get_lowest_layer(ws_).expires_never(); 
  or
    // Enable a new timeout
    beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(YYYY));

  Step 2> call another asynchronous function
  Step 3> beast::get_lowest_layer(ws_).expires_never();
}

Does this make sense?

Thank you


Solution

  • Question 1

    Yes that's correct. The linked page has the confirmation:

    // The timer is still running. If we don't want the next
    // operation to time out 30 seconds relative to the previous
    // call  to `expires_after`, we need to turn it off before
    // starting another asynchronous operation.
    
    stream.expires_never();
    

    Question 2

    That looks fine. The only subtleties I can think of are

    • often, because of Thread Safety often the initiation as well as the completion happen on the same (implicit) strand.

      If that's the case, then in your completion handler example, the expires_never(); would be redundant.

    • If the completion handler is not on the same strand, you want to actively avoid touching the expiry, because that would be a data race

    • An alternative pattern is to set the expiry only once for a lengthier episode (e.g. an multi-message conversation between client/server). Obviously in this pattern, nobody would touch the expiry after initial setting. This seems pretty obvious, but I thought I'd mention it before someone casts this pattern in stone to never think about it again.

    Always do what you need, prefer simple code. I think your basic understanding of the feature is right. (No wonder, this documentation is a piece of art).