c++httpboostasiobeast

Boost.Beast `tcp_stream`'s `async_connect` waits full duration with `use_future`


I'm writing a connection manager class that should establish connection to the server and send several HTTP requests using Boost.Beast. It has a connect method that should return true if connection was successful and false otherwise. I am using tcp_stream and its async_connect method with boost::asio::use_future to wait_for a certain type and check if connection failed to establish in time.

My connect method has the following code:

// ...

const auto resolve = this->resolver.resolve(this->host, this->port);

auto connect_future = this->stream.async_connect(resolve, boost::asio::use_future);

switch (connect_future.wait_for(boost::asio::chrono::seconds{5})) {
  // Do the work
}

// ...

The event loop for io_context associated with both reolver and stream has its run method called constantly in event loop thread. It establishes connection and works, but always waits full 5 seconds until proceeding. What am I doing wrong?


edit: While trying to make a minimal reproducible example, I've stumble upon the following: code exits cleanly when compiled with -DWORKING and prints Timeout without it!

#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/use_future.hpp>
#include <boost/asio/strand.hpp>
#include <atomic>
#include <iostream>
#include <string>

// Performs an HTTP GET and prints the response
int main(int argc, char** argv)
{

    namespace net = boost::asio;
    namespace beast = boost::beast;

    net::io_context ioc;

    std::atomic_bool running = true;
    std::thread loop([&ioc, &running] {
        while (running) {
            ioc.run();
        }
    });

    net::ip::tcp::resolver resolver(net::make_strand(ioc));
    beast::tcp_stream stream(net::make_strand(ioc));

    const auto resolve = resolver.resolve("127.0.0.1", "8080");

    auto fut = stream.async_connect(resolve, net::use_future);

    switch (fut.wait_for(net::chrono::seconds(5))) {
        case std::future_status::timeout:
            std::cerr << "Timelimit" << std::endl;
            break;

        default: break;
    }

    running = false;
    loop.join();

    stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both);
    ioc.stop();
    return 0;
}

edit2: Something more resembling my actual code. This is not affected by the issue, however. Am I running into an UB?

#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/use_future.hpp>
#include <boost/asio/strand.hpp>
#include <atomic>
#include <iostream>
#include <string>

// Performs an HTTP GET and prints the response
int main(int argc, char** argv)
{

    namespace net = boost::asio;
    namespace beast = boost::beast;

    net::io_context ioc;

    std::atomic_bool running = true;
    std::thread loop([&ioc, &running] {
        while (running) {
            ioc.run();
        }
    });

    net::ip::tcp::resolver resolver(net::make_strand(ioc));
    beast::tcp_stream stream(net::make_strand(ioc));

    const auto resolve = resolver.resolve("127.0.0.1", "8080");

    auto fut = stream.async_connect(resolve, net::use_future);

    switch (fut.wait_for(net::chrono::seconds(5))) {
        case std::future_status::timeout:
            std::cerr << "Timelimit" << std::endl;
            break;

        default: break;
    }

    running = false;
    loop.join();

    stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both);
    ioc.stop();
    return 0;
}

Solution

  • Futures only make sense if the service is running on some different thread(s).

    Your thread is not guaranteed to work:

    while(running)
    {
        ioc.run();
    }
    

    The documentation explains that if run() runs out of work, restart() would be required to restart it. This means that the thread will just run a tight loop without effect (except for heating up your CPU).

    Instead use a work_guard or indeed thread_pool:

    net::io_context ioc;
    auto            work = make_work_guard(ioc);
    std::thread     loop([&ioc, &running] { ioc.run(); });
    

    Or

    net::thread_pool ioc(1); // later ioc.join()
    

    This has the benefit that you don't need to fix your hand-written thread to deal with exceptions (Should the exception thrown by boost::asio::io_service::run() be caught?)