Search code examples
c++unixboostasiodeadline-timer

Why does boost deadline_timer used as timeout for async_connect not work on unix?


I have a program that uses boost asio to connect to 3 TCP sockets asynchronously using a deadline_timer as a connect timeout. On windows everything works as expected. The connection times out after 5 seconds. However, on Unix(Ubuntu on WSL, Linux Mint VM, macOS) the connectDeadline never fires. The async_connect operation just runs on forever. Why does this not work and how can I make this work on Unix as well?

Code: Note: connect is called from the main thread (which is also a GUI thread).

#include "NetManager.h"

NetManager::NetManager(NetManagerListener& listener) :  listener(listener),
connectDeadline(io),
                                                        socket1(io),
                                                        socket2(io),
                                                        socket3(io),
                                                        asioThread(&NetManager::handleAsioOperations, this){

}
NetManager::~NetManager() {
    running = false;
    io.stop();
    asioThread.join();
}

void NetManager::connect(){
    connectCounter = 0;
    hasHandledConnectError = false;
    socket1.async_connect(
            tcp::endpoint(boost::asio::ip::address::from_string(IP_STRING), PORT_1),
            boost::bind(&NetManager::handleConnect, this, _1));
    socket2.async_connect(
            tcp::endpoint(boost::asio::ip::address::from_string(IP_STRING), PORT_2),
            boost::bind(&NetManager::handleConnect, this, _1));
    socket3.async_connect(
            tcp::endpoint(boost::asio::ip::address::from_string(IP_STRING), PORT_3),
            boost::bind(&NetManager::handleConnect, this, _1));
    connectDeadline.expires_from_now(boost::posix_time::seconds(CONNECT_TIMEOUT));
    connectDeadline.async_wait(boost::bind(&NetManager::handleConnectTimeout, this, _1));
}

void NetManager::disconnect(){
    //NOTE: Close also cancels incomplete async operations
    socket1.close();
    socket2.close();
    socket3.close();
}

////////////////////////////////////////////////////////////////////////
/// ASIO Handlers
////////////////////////////////////////////////////////////////////////
void NetManager::handleAsioOperations(){
    while(running){
        io.run(); // Run any async operations
    }
}

void NetManager::handleConnect(const boost::system::error_code &ec){
    // When connections are canceled the handler is called with operation_aborted. No need to respond to that.
    if(ec && ec != boost::asio::error::operation_aborted && !hasHandledConnectError){
        hasHandledConnectError = true; // Likely to be 3 repeated errors. Make sure to only handle the first one
        cerr << "Connect Failed: " << ec.message() << endl;
        connectDeadline.cancel(); // Don't fire the timeout
        disconnect(); // Disconnect any already connected sockets
        connectedToRobot = false;
        listener.onConnect(false);
    }else if (!ec){
        connectCounter++;
    }

    if(connectCounter == 3){
        cout << "Successful connect" << endl;
        connectDeadline.cancel(); // Don't fire the timeout
        connectedToRobot = true;
        listener.onConnect(true);
    }
}

void NetManager::handleConnectTimeout(const boost::system::error_code &ec){
    if(ec != boost::asio::error::operation_aborted){
        cerr << "Connect timed out." << endl;
        disconnect(); // Disconnect any already connected sockets
        connectedToRobot = false;
        listener.onConnect(false);
    }
}

EDIT:

Confusingly, this works fine on Unix OSes:

#include <boost/asio.hpp>                                                                                                                    
#include <boost/asio/deadline_timer.hpp>                                                                                                     
#include <iostream>                                                                                                                          
#include <thread>                                                                                                                            

using namespace boost::asio;                                                                                                                 
using namespace boost::asio::ip;                                                                                                             

int main(){                                                                                                                                  
        io_service io;                                                                                                                       
        deadline_timer timer1(io);                                                                                                           
        tcp::socket sock(io);                                                                                                                

        timer1.expires_from_now(boost::posix_time::seconds(3));                                                                              
        sock.async_connect(tcp::endpoint(boost::asio::ip::address::from_string("10.50.30.1"), 8090), [](const boost::system::error_code &ec){
                std::cout << "SocketError: " << ec.message() << std::endl;                                                                   
        });                                                                                                                                  
        timer1.async_wait([&](const boost::system::error_code &ec){                                                                          
                std::cout << "First timer" << std::endl;                                                                                     
                sock.close();                                                                                                                
        });                                                                                                                                  
        std::thread worker([&](){                                                                                                            
                while(true){                                                                                                                 
                        io.run();                                                                                                            
                }                                                                                                                            
        });                                                                                                                                  
        worker.detach();                                                                                                                     
        while(true){} // Simulate the unavailable main (GUI) thread                                                                          
}  

Output:

First timer
SocketError: Operation canceled

Solution

  • boost::io_service::run stops if there is no work. You should not be calling it in a loop like this because as the documentation state:

    A normal exit from the run() function implies that the io_service 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 reset().

    You don't show when you call connect() in your first example, I assume it is not long after, so the fact that it works or not comes down to how fast the thread is launched and the run() call executed.

    You can circumvent this behavior by using boost::io_service::work which will prevent the io_service from running out of work.