c++boostboost-asiobeast

boost/beast io_context, is it possible to call it for a second run()


I've got simple io_context object which is used to initialize ssl_stream object (using async_resolve, async_connect and async_handshake).

On a different scope, it's used to call async_read and async_write to pass IO in that connection.

the async calls are performed from within coroutine (boost::asio::spawn(io_context_, [&](boost::asio::yield_context yield)). each one of the stages above is executed on a different such coroutine. In order to execute the coroutine, the underlying io_context of the ssl_stream should be in run() method.

However, since those 2 methods are separated, than a single run() stage wouldn't be enough. After the connection initialization will be finished, the first run() will be terminated, so a second run() instance should be called right after the second coroutine (that does the IO ops) is called)

However, I observed that the second run goes out immediately, and doesn't perform the recently inserted spawn. Any idea how to overcome this scenario ? or the only alternative is to run the whole connection lifecycle in single coroutine, or call the run on a separated thread that never quits...

Here's the semi-pseudo code of my scenario :

boost::asio::io_context io_context_;
std::optional<boost::beast::tcp_stream> stream_;
std::optional<boost::asio::ip::tcp::resolver> resolver_;

//STAGE 1

boost::asio::spawn(io_context_, [&](boost::asio::yield_context yield) {
    results = resolver_->async_resolve(host_,port_ , yield);
    ssl_stream_->next_layer().async_connect(results, yield);
    ssl_stream_->async_handshake(ssl::stream_base::client, yield);
}
try {
 io_context_.run();
} catch (...) {
}

//STAGE 2

beast::flat_buffer buffer;
http::response<http::dynamic_body> res;

boost::asio::spawn(io_context_, [&](boost::asio::yield_context yield) {
  beast::get_lowest_layer(*ssl_stream_).expires_after(kOpTimeout);
  auto sent = http::async_write(*ssl_stream_, beast_request, yield);
  auto read = http::async_read(*ssl_stream_, buffer, res, yield);
});

try { 
  io_context_.run();
  return res;
} catch (...) {
}


Solution

  • It is possible, but a-typical. As documented, in order to be able to re-run after the service ran out of work (I.e. returning 0 handlers when executed), you need to call restart() (previously reset()):

    A normal exit from the run() function implies that the io_context object is stopped (the stopped() function returns true). Subsequent calls to run(), run_one(), poll() or poll_one() will return immediately unless there is a prior call to restart().

    Demo

    Here is a demo based on your snippet:

    #include <boost/asio.hpp>
    #include <boost/asio/spawn.hpp>
    #include <boost/beast.hpp>
    #include <boost/beast/ssl.hpp>
    #include <iostream>
    #include <optional>
    
    namespace net   = boost::asio;
    namespace ssl   = net::ssl;
    namespace beast = boost::beast;
    namespace http  = beast::http;
    using net::ip::tcp;
    
    using namespace std::chrono_literals;
    
    struct X {
        std::string host_ = "www.example.com";
        std::string port_ = "https";
    
        static auto constexpr kOpTimeout = 3s;
    
        net::io_context                                     io_context_;
        ssl::context                                        ctx_{ssl::context::sslv23_client};
        std::optional<beast::ssl_stream<beast::tcp_stream>> ssl_stream_{std::in_place,
                                                                        io_context_, ctx_};
        std::optional<tcp::resolver> resolver_{std::in_place, io_context_};
    
        X() { ctx_.set_default_verify_paths(); }
    
        void STAGE1() {
            net::spawn(io_context_, [&](net::yield_context yield) {
                try {
                    auto results = resolver_->async_resolve(host_, port_, yield);
                    ssl_stream_->next_layer().async_connect(results, yield);
                    ssl_stream_->async_handshake(ssl::stream_base::client, yield);
                } catch (std::exception const& e) {
                    std::cerr << "Whoops in " << __LINE__ << ": " << e.what() << std::endl;
                    throw;
                }
            });
    
            if (io_context_.stopped())
                io_context_.restart();
    
            try {
                io_context_.run();
            } catch (std::exception const& e) {
                std::cerr << "Whoops in " << __LINE__ << ": " << e.what() << std::endl;
                throw;
            }
        }
    
        auto STAGE2() {
            beast::flat_buffer                 buffer;
            http::response<http::dynamic_body> res;
    
            http::request<http::empty_body> beast_request(http::verb::get, "/", 11);
            beast_request.set(http::field::host, "www.example.com");
    
            net::spawn(io_context_, [&](net::yield_context yield) {
                try {
                    beast::get_lowest_layer(*ssl_stream_).expires_after(kOpTimeout);
                    /*auto sent =*/ http::async_write(*ssl_stream_, beast_request, yield);
                    /*auto read =*/ http::async_read(*ssl_stream_, buffer, res, yield);
                } catch (std::exception const& e) {
                    std::cerr << "Whoops in " << __LINE__ << ": " << e.what() << std::endl;
                    throw;
                }
            });
    
            if (io_context_.stopped())
                io_context_.restart();
    
            try {
                io_context_.run();
            } catch (std::exception const& e) {
                std::cerr << "Whoops in " << __LINE__ << ": " << e.what() << std::endl;
                throw;
            }
            return res;
        }
    };
    
    int main() {
        X x;
    
        x.STAGE1();
        std::cout << x.STAGE2();
    }
    

    Prints, locally:

    HTTP/1.1 200 OK
    Age: 533333
    Cache-Control: max-age=604800
    Content-Type: text/html; charset=UTF-8
    Date: Sun, 18 Dec 2022 21:50:21 GMT
    Etag: "3147526947+ident"
    Expires: Sun, 25 Dec 2022 21:50:21 GMT
    Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
    Server: ECS (bsa/EB20)
    Vary: Accept-Encoding
    X-Cache: HIT
    Content-Length: 1256
    
    <!doctype html>
    <html>
    <head>
        <title>Example Domain</title>
    
        <meta charset="utf-8" />
        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <style type="text/css">
        body {
            background-color: #f0f0f2;
            margin: 0;
            padding: 0;
            font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
            
        }
        div {
            width: 600px;
            margin: 5em auto;
            padding: 2em;
            background-color: #fdfdff;
            border-radius: 0.5em;
            box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
        }
        a:link, a:visited {
            color: #38488f;
            text-decoration: none;
        }
        @media (max-width: 700px) {
            div {
                margin: 0 auto;
                width: auto;
            }
        }
        </style>    
    </head>
    
    <body>
    <div>
        <h1>Example Domain</h1>
        <p>This domain is for use in illustrative examples in documents. You may use this
        domain in literature without prior coordination or asking for permission.</p>
        <p><a href="https://www.iana.org/domains/example">More information...</a></p>
    </div>
    </body>
    </html>
    

    Notes

    Again, make buffer a member, because its context will matter for any subsequent traffic on the connection.

    Also, don't use run() to find out when things are "done". You can use futures, or any other way of synchronization:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <boost/asio/spawn.hpp>
    #include <boost/beast.hpp>
    #include <boost/beast/ssl.hpp>
    #include <iostream>
    #include <optional>
    
    namespace net   = boost::asio;
    namespace ssl   = net::ssl;
    namespace beast = boost::beast;
    namespace http  = beast::http;
    using net::ip::tcp;
    
    using namespace std::chrono_literals;
    using duration = std::chrono::steady_clock::duration;
    
    struct Client {
    
        Client(net::any_io_executor ex) : ssl_stream_{ex, ctx_} {
            ctx_.set_default_verify_paths();
        }
    
        void connect(std::string host, std::string port = "https") {
            host_ = host;
    
            std::packaged_task<void(std::string, std::string, net::yield_context)> task{
                [this](std::string host, std::string port, net::yield_context yield) {
                    tcp::resolver resolver_{ssl_stream_.get_executor()};
                    auto          results = resolver_.async_resolve(host, port, yield);
                    ssl_stream_.next_layer().async_connect(results, yield);
                    ssl_stream_.async_handshake(ssl::stream_base::client, yield);
                }};
            auto fut = task.get_future();
    
            net::spawn(ssl_stream_.get_executor(),
                       std::bind(std::move(task), std::move(host), std::move(port),
                                 std::placeholders::_1));
    
            return fut.get();
        }
    
        using Response = http::response<http::dynamic_body>;
        Response makeRequest(std::string target, duration kOpTimeout = 300ms) {
            http::request<http::empty_body> beast_request(http::verb::get, target, 11);
            beast_request.set(http::field::host, host_);
    
            std::packaged_task<Response(net::yield_context)> task{
                [kOpTimeout, req = std::move(beast_request), this](net::yield_context yield) {
                    Response res;
    
                    beast::get_lowest_layer(ssl_stream_).expires_after(kOpTimeout);
                    /*auto sent =*/http::async_write(ssl_stream_, req, yield);
                    /*auto read =*/http::async_read(ssl_stream_, buffer_, res, yield);
    
                    return res;
                }};
            auto fut = task.get_future();
    
            net::spawn(ssl_stream_.get_executor(), std::move(task));
    
            return fut.get();
        }
    
      private:
        ssl::context                         ctx_{ssl::context::sslv23_client};
        beast::ssl_stream<beast::tcp_stream> ssl_stream_;
    
        beast::flat_buffer buffer_;
        std::string        host_;
    
    };
    
    int main() {
        net::thread_pool ioc(1);    // 1 thread is enough
        Client x(make_strand(ioc)); // strand only required if you have multiple IO threads
    
        x.connect("httpbin.org");
        std::cout << x.makeRequest("/delay/2", 3s) << std::endl;
        std::cout << "Second request will time out: " << std::endl;
        std::cout << x.makeRequest("/delay/5", 1s); // timeout
    
        ioc.join(); // wait for service to complete
    }
    

    Locally:

    enter image description here