2

Note: Not sure if this is due to Asio bug, Azmq bug or my own ignorance.

I am using an Azmq library to read off a socket, this library uses its own type of socket (azmq::socket) which I am pretty sure is a wrapper for a boost::asio::socket. The flow of execution is the following:

Timer function for reference

auto timer(std::chrono::steady_clock::duration dur) -> asio::awaitable<void> {
    asio::steady_timer timer(co_await asio::this_coro::executor);
    timer.expires_after(dur);
    co_await timer.async_wait(asio::use_awaitable);
}
auto val = co_await(azmq::async_receive(socket_, asio::buffer(buffer), asio::use_awaitable) || timer(std::chrono::seconds{1}));
template<class CompletionToken, class MutableBufferSequence>
auto async_receive(azmq::socket &socket, MutableBufferSequence const &buffers, CompletionToken &&token) -> BOOST_ASIO_INITFN_RESULT_TYPE(CompletionToken, void(boost::system::error_code, size_t)) {
    return boost::asio::async_initiate<CompletionToken, void(boost::system::error_code, size_t)>(async_receive_initiation<MutableBufferSequence>{socket, buffers}, token);
}
template<typename MutableBufferSequence>
struct async_receive_initiation {
  azmq::socket &socket;
  MutableBufferSequence const &buffers;

  template<typename CompletionHandler>
  void operator()(CompletionHandler &&completion_handler) {
      auto executor = boost::asio::get_associated_executor(
          completion_handler, socket.get_executor());
      socket.async_receive(buffers, boost::asio::bind_executor(executor,
                                                            std::bind(std::forward<CompletionHandler>(completion_handler), std::placeholders::_1, std::placeholders::_2)));
  }
}

template<typename MessageReadHandler>
void async_receive(MessageReadHandler && handler, flags_type flags = 0) {
        using type = detail::receive_op<MessageReadHandler>;
        get_service().enqueue<type>(get_implementation(), detail::socket_service::op_type::read_op, std::forward<MessageReadHandler>(handler), flags);
}

The problem arises when the timer fires, the async_receive is not cancelled. I have read the Asio documentation but to no avail. What is the problem? Is the async_receive missing a method on it? I have tried to read up on what actually happens when the operation is cancelled but I have not found much. All help is appreciated.

I have tried changing the completion tokens, and defining cancellation function on to the struct which defines the async handlers, but nothing seems to work.

Minimal example of behaviour:

#pragma once

#include <azmq/socket.hpp>
#include <boost/asio.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <iostream>

namespace asio = boost::asio;
using namespace asio::experimental::awaitable_operators;

auto timer(std::chrono::steady_clock::duration dur) -> asio::awaitable<void> {
    asio::steady_timer timer(co_await asio::this_coro::executor);
    timer.expires_after(dur);
    co_await timer.async_wait(asio::use_awaitable);
}

auto foo(asio::io_context &ctx) -> asio::awaitable<void> {

    azmq::sub_socket socket_{ctx};
    socket_ = azmq::sub_socket(socket_.get_io_context(), true);
    boost::system::error_code error_code;
    std::string const socket_path{"ipc:///tmp/not_real"};
    if (socket_.connect(socket_path, error_code)) {
        std::exit(-1);
    }
    if (socket_.set_option(azmq::socket::subscribe(""), error_code)) {
        std::exit(-1);
    }

    std::array<std::byte, 1024> buffer{};

    std::cout << "waiting for async_receive" << std::endl;
    co_await(azmq::async_receive(socket_, asio::buffer(buffer), asio::use_awaitable) || timer(std::chrono::seconds{1}));

    std::cout << "async_receive finished" << std::endl;

}

auto main() -> int {

    boost::asio::io_context ctx{};
    boost::asio::co_spawn(ctx, foo(ctx), asio::detached);
    ctx.run();

    return 0;
}

magni_mar
  • 21
  • 3
  • Please add a self-contained minimal example. Right now it's way too much work to create one for me. Best idea I have is: add exception handling inside `timer` function (see e.g. https://stackoverflow.com/a/75651413/85371) – sehe Aug 17 '23 at 13:05
  • Sorry, I have added a minimal example now. – magni_mar Aug 17 '23 at 14:19

1 Answers1

1

I've changed the minimal reproducer to not require azmq:

Live On Coliru

#include <boost/asio.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <iostream>

namespace asio = boost::asio;
using asio::ip::tcp;
using namespace asio::experimental::awaitable_operators;
using namespace std::chrono_literals;

asio::awaitable<void> timer(std::chrono::steady_clock::duration dur) {
    asio::steady_timer timer(co_await asio::this_coro::executor);
    timer.expires_after(dur);
    co_await timer.async_wait(asio::use_awaitable);
}

asio::awaitable<void> foo() {
    auto ex = co_await asio::this_coro::executor;
    tcp::socket socket_{ex};

    boost::system::error_code ec;
    auto token = redirect_error(asio::use_awaitable, ec);

    co_await socket_.async_connect({{}, 7878}, token);

    std::cerr << "connect: " << ec.message() << std::endl;
    if (ec.failed()) std::exit(-1);

    std::array<std::byte, 1024> buffer{};
    auto r = co_await (async_read(socket_, asio::buffer(buffer), token) || timer(1s));

    if (r.index()) {
        std::cout << "timed out" << std::endl;
    } else {
        std::cout << "async_receive: " << ec.message() << ", " << get<0>(r) << " bytes transferred"
                  << std::endl;
    }
}

int main() {
    asio::io_context ctx;

    co_spawn(ctx, foo, asio::detached);
    ctx.run();
}

For me it already works as expected:

enter image description here

I think for more complicated scenarios (especially regarding threads running execution context(s)) it becomes mandatory handle the error inside timer:

asio::awaitable<void> timer(std::chrono::steady_clock::duration dur) try {
    asio::steady_timer timer(co_await asio::this_coro::executor);
    timer.expires_after(dur);
    co_await timer.async_wait(asio::use_awaitable);
} catch (boost::system::system_error const&) {
}

Or indeed use redirect_error as shown above, or alternatives, e.g. Use error codes with C++20 Coroutines i.e. awaitables with Boost ASIO

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Thank you for a quick response but this is not quite what I was looking for. I believe the problem does not lie with Boost.Asio but rather with how Azmq is using Boost.Asio, this is the problem I need to fix. So I need a solution that uses Azmq. – magni_mar Aug 17 '23 at 15:11
  • 1
    Fair enough. I might take the time to learn Azmq later. To be completely fair, I fear the short summary will be that Azmq didn't support `associated_cancellation_slot` - or not correctly. – sehe Aug 17 '23 at 16:57