1

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;
}
  • please show a [mre] – Alan Birtles Apr 28 '23 at 14:33
  • 1
    @AlanBirtles I've added some code to the question. It's not a minimal reproducible example, as I struggle to reproduce this behavior. It is, however, curious because I'm (the way I see it) doing essentially the same thing in both cases. I guess I just don't understand Boost.Beast – GregTheMadMonk Apr 28 '23 at 15:14
  • What does defining `WORKING` do in your second example? I don't see any `#ifdef`s – Alan Birtles Apr 28 '23 at 18:23
  • yeah, the code is wrong for some reason, and I left for the weekend without checking it twice. The macro was supposed to switch between defining `running` and `loop` before and fter `resolver` and `stream` – GregTheMadMonk May 01 '23 at 12:55

1 Answers1

1

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?)

sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    Looks like this is it. I still have no idea why was it waiting for 5 seconds every time, but adding a work guard and changing the event loop function helped. Didn't go for thread_pool since I needed to restart io_context in case of a failed connection to possibly retry it – GregTheMadMonk May 01 '23 at 12:53