Search code examples
c++boostboost-asioboost-coroutineboost-timer

How to specify `boost::asio::yield_context` with timeout?


I would like to learn how to pass timeout timer to boost::asio::yield_context.

Let's say, in terms of Boost 1.80, there is smth like the following:

#include <boost/asio/io_context.hpp>
#include <boost/asio/spawn.hpp>

void async_func_0(boost::asio::yield_context yield) {
  async_func_1(yield);
}

void async_func_1(boost::asio::yield_context) {
}

int main() {
  boost::asio::io_context ioc;
  boost::asio::spawn(ioc.get_executor(), &async_func_0);
  ioc.run();
  return 0;  
}

Let's imaging that the async_func_1 is quite a burden, it is async by means of boost::coroutines (since boost::asio does not use boost::coroutines2 for some unknown reason) and it can work unpredictably long, mostly on io operations.

A good idea would be to specify the call of async_func_1 with a timeout so that if the time passed it must return whatever with an error. Let's say at the nearest use of boost::asio::yield_context within the async_func_1.

But I'm puzzled how it should be expressed in terms of boost::asio.

P.S. Just to exemplify, in Rust it would be smth like the following:

use std::time::Duration;
use futures_time::FutureExt;

async fn func_0() {
  func_1().timeout(Duration::from_secs(60)).await;
}

async fn func_1() {
}

#[tokio::main]
async fn main() {
  tokio::task::spawn(func_0());
}

Solution

  • In Asio cancellation and executors are separate concerns.

    That's flexible. It also means you have to code your own timeout.

    One very rough idea:

    #include <boost/asio.hpp>
    #include <boost/asio/spawn.hpp>
    #include <iostream>
    namespace asio = boost::asio;
    using boost::asio::yield_context;
    using namespace std::chrono_literals;
    using boost::system::error_code;
    
    static std::chrono::steady_clock::duration s_timeout = 500ms;
    
    
    template <typename Token>
    void async_func_1(Token token) {
        error_code ec;
    
        // emulating a long IO bound task
        asio::steady_timer work(get_associated_executor(token), 1s);
        work.async_wait(redirect_error(token, ec));
    
        std::cout << "async_func_1 completion: " << ec.message() << std::endl;
    }
    
    void async_func_0(yield_context yield) {
        asio::cancellation_signal cancel;
    
        auto cyield = asio::bind_cancellation_slot(cancel.slot(), yield);
    
        std::cout << "async_func_0 deadline at " << s_timeout / 1.0s << "s" << std::endl;
    
        asio::steady_timer deadline(get_associated_executor(cyield), s_timeout);
        deadline.async_wait([&](error_code ec) {
            std::cout << "Timeout: " << ec.message() << std::endl;
            if (!ec)
                cancel.emit(asio::cancellation_type::terminal);
        });
    
        async_func_1(cyield);
    
        std::cout << "async_func_0 completion" << std::endl;
    }
    
    int main(int argc, char** argv) {
        if (argc>1)
            s_timeout = 1ms * atoi(argv[1]);
    
        boost::asio::io_context ioc;
        spawn(ioc.get_executor(), async_func_0);
    
        ioc.run();
    }
    

    No online compilers that accept this¹ are able to run this currently. So here's local output:

    for t in 150 1500; do time ./build/sotest "$t" 2>"$t.trace"; ~/custom/superboost/libs/asio/tools/handlerviz.pl < "$t.trace" | dot -T png -o trace_$t.png; done
    
    async_func_0 deadline at 0.15s
    Timeout: Success
    async_func_1 completion: Operation canceled
    async_func_0 completion
    
    real    0m0,170s
    user    0m0,009s
    sys     0m0,011s
    async_func_0 deadline at 1.5s
    async_func_1 completion: Success
    async_func_0 completion
    Timeout: Operation canceled
    
    real    0m1,021s
    user    0m0,011s
    sys     0m0,011s
    

    And the handler visualizations:

    ¹ wandbox, coliru, CE


    Road From Here

    You'll probably say this is cumbersome. Compared to your Rust library feature it is. To library this in Asio you could

    • derive your own completion token from type yield_context, adding the behaviour you want
    • make a composing operation (e.g. using deferred)