[boost] Need cancel() on async_resolve() to callback the resolve handler immediately

834 views
Skip to first unread message

sandeep m.v via Boost

unread,
Aug 23, 2021, 10:46:02 AM8/23/21
to bo...@lists.boost.org, sandeep m.v
Hi All,

I'm looking for a solution for below problem.
We're using async_resolve() to perform DNS resolution using
boost::asio::ip::tcp::resolver.
In few instances probably during network connectivity issues, async_resolve
is taking more time.
When our application timeouts in 15seconds while waiting for DNS
resolution, cancel() is called on resolver. But, that doesn't callback to
resolve handle immediately.
When I tried to search for this, it looks like, when a cancel() is called
on resolver, that doesn't cancel ongoing actual resolution but may cancel
if there are any other pending asynchronous action.
It's taking 40seconds at times to call resolve handler even when failed to
resolve host and even after calling cancel on it.
My application waits until resolve handler is called after cancel() to
proceed further processing.
Is it possible to stop that async_resolve() and get callback to resolve
handle immediately after cancel()?
If that's impossible, is there a way that we can stop that doesn't callback
resolve handler later? Is it possible to close underlying socket related to
async_resolve from my application?
Thank you in advance.

Regards,
Sandeep

_______________________________________________
Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost

sandeep m.v via Boost

unread,
Aug 24, 2021, 3:08:56 PM8/24/21
to bo...@lists.boost.org, sandeep m.v
Hi,
Can someone please help me with this?


On Mon, Aug 23, 2021 at 7:54 PM sandeep m.v <venkata....@gmail.com>
wrote:
--
Regards,
Sandeep
Mob# 9000707098

Vinnie Falco via Boost

unread,
Aug 24, 2021, 3:56:27 PM8/24/21
to boost@lists.boost.org List, Vinnie Falco
On Tue, Aug 24, 2021 at 12:09 PM sandeep m.v via Boost
<bo...@lists.boost.org> wrote:
> Can someone please help me with this?

Are you by chance using Windows? What version?

Thanks

Richard Hodges via Boost

unread,
Aug 24, 2021, 4:00:51 PM8/24/21
to bo...@lists.boost.org, Richard Hodges
On Linux async_resolve is currently implemented in terms of a spawned
thread which calls a synchronous function. It has a 30s timeout and can’t
be cancelled.

On Tue, 24 Aug 2021 at 21:56, Vinnie Falco via Boost <bo...@lists.boost.org>
wrote:

--
Richard Hodges
hodg...@gmail.com
office: +44 2032 898 513
home: +376 861 195
mobile: +376 380 212

Richard Hodges via Boost

unread,
Aug 25, 2021, 4:29:09 AM8/25/21
to sandeep m.v, Richard Hodges, boost@lists.boost.org List
There are a couple of convoluted ways to build cancellation into the linux
resolve() activity.
The problem is that the sockets and timers used for this are entirely
embedded in the OS-shipped library layer and not accessible to user code.
It's a bit of a mess that has never been cleaned up. There isn't much that
Asio can do about it.

At present the "cleanest" way is to fork a child process to do the resolve
call for you and async_wait on a pipe from that child. If you want to
cancel it, send the child a SIGKILL which will result in the wait on the
pipe completing with an error.

Ghastly I know...


On Wed, 25 Aug 2021 at 08:30, sandeep m.v <venkata....@gmail.com>
wrote:

> Thank you for the response.
>
> We build our application for multiple platforms, Windows, Linux, Android
> and iOS.
> If it is not possible to cancel, is there a way to close internal socket,
> so that it wont callback resolve handler?
> By that way, we can at least go ahead with our processing after our
> internal timeout assuming callback never happen later.
> Is that something possible?
>
>
>
> On Wed, Aug 25, 2021 at 1:29 AM Richard Hodges via Boost <


> bo...@lists.boost.org> wrote:
>
>> On Linux async_resolve is currently implemented in terms of a spawned
>> thread which calls a synchronous function. It has a 30s timeout and can’t
>> be cancelled.
>>
>> On Tue, 24 Aug 2021 at 21:56, Vinnie Falco via Boost <
>> bo...@lists.boost.org>
>> wrote:
>>
>> > On Tue, Aug 24, 2021 at 12:09 PM sandeep m.v via Boost
>> > <bo...@lists.boost.org> wrote:
>> > > Can someone please help me with this?
>> >
>> > Are you by chance using Windows? What version?
>> >
>> > Thanks
>> >
>> > _______________________________________________
>> > Unsubscribe & other changes:
>> > http://lists.boost.org/mailman/listinfo.cgi/boost
>> >
>> --
>> Richard Hodges
>> hodg...@gmail.com
>> office: +44 2032 898 513
>> home: +376 861 195
>> mobile: +376 380 212
>>
>> _______________________________________________
>> Unsubscribe & other changes:
>> http://lists.boost.org/mailman/listinfo.cgi/boost
>>
>
>

> --
> Regards,
> Sandeep
> Mob# 9000707098
>

_______________________________________________

Andrey Semashev via Boost

unread,
Aug 25, 2021, 6:25:05 AM8/25/21
to bo...@lists.boost.org, Andrey Semashev
On 8/25/21 11:27 AM, Richard Hodges via Boost wrote:
> There are a couple of convoluted ways to build cancellation into the linux
> resolve() activity.
> The problem is that the sockets and timers used for this are entirely
> embedded in the OS-shipped library layer and not accessible to user code.
> It's a bit of a mess that has never been cleaned up. There isn't much that
> Asio can do about it.
>
> At present the "cleanest" way is to fork a child process to do the resolve
> call for you and async_wait on a pipe from that child. If you want to
> cancel it, send the child a SIGKILL which will result in the wait on the
> pipe completing with an error.
>
> Ghastly I know...

On Linux, there is also getaddrinfo_a:

https://man7.org/linux/man-pages/man3/getaddrinfo_a.3.html

which allows to perform name resolution asynchronously, with
cancellation. It's a GNU extension. I wonder if it can be incorporated
into Boost.ASIO.

PS: Please, don't top-post.

Andrey Semashev via Boost

unread,
Aug 25, 2021, 6:28:35 AM8/25/21
to bo...@lists.boost.org, Andrey Semashev
On 8/25/21 1:23 PM, Andrey Semashev wrote:
> On 8/25/21 11:27 AM, Richard Hodges via Boost wrote:
>> There are a couple of convoluted ways to build cancellation into the
>> linux
>> resolve() activity.
>> The problem is that the sockets and timers used for this are entirely
>> embedded in the OS-shipped library layer and not accessible to user code.
>> It's a bit of a mess that has never been cleaned up. There isn't much
>> that
>> Asio can do about it.
>>
>> At present the "cleanest" way is to fork a child process to do the
>> resolve
>> call for you and async_wait on a pipe from that child. If you want to
>> cancel it, send the child a SIGKILL which will result in the wait on the
>> pipe completing with an error.
>>
>> Ghastly I know...
>
> On Linux, there is also getaddrinfo_a:
>
> https://man7.org/linux/man-pages/man3/getaddrinfo_a.3.html
>
> which allows to perform name resolution asynchronously, with
> cancellation. It's a GNU extension. I wonder if it can be incorporated
> into Boost.ASIO.

It seems, it's even been suggested before:

https://github.com/chriskohlhoff/asio/issues/449

Richard Hodges via Boost

unread,
Aug 25, 2021, 1:10:21 PM8/25/21
to boost@lists.boost.org List, Richard Hodges
> >> At present the "cleanest" way is to fork a child process to do the
> >> resolve
> >> call for you and async_wait on a pipe from that child. If you want to
> >> cancel it, send the child a SIGKILL which will result in the wait on the
> >> pipe completing with an error.
> >>
> >> Ghastly I know...
>
>
Here is a possible workaround that I've hacked together. Uses C++20
coroutines and latest boost.asio.

example output:
$ resolve
timeout: 1ms : exception Connection timed out
timeout: 5000ms : timeout: 5000ms : 142.250.184.14:80,
[2a00:1450:4003:80f::200e]:80
timeout: 20000ms : timeout: 20000ms : 142.250.184.14:80,
[2a00:1450:4003:80f::200e]:80

Here's the code:

//
// Copyright (c) 2021 Richard Hodges (hodg...@gmail.com)
//
// Distributed under the Boost Software License, Version 1.0. (See
accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
//

#include <boost/asio.hpp>
#include <boost/asio/experimental/as_tuple.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <sys/wait.h>

#include <array>
#include <iostream>
#include <string>
#include <vector>

namespace asio = boost::asio;
namespace asioex = asio::experimental;
using boost::system::error_code;
using boost::system::generic_category;
using boost::system::system_error;

/// Serialise enough of a resolver's results into a null-separarted
sequence of string segments
std::string
serialise(asio::ip::tcp::resolver::results_type const &results)
{
std::string result;
auto host = results->host_name();
auto service = results->service_name();
result.insert(result.end(), host.c_str(), host.c_str() + host.size() +
1);
result.insert(result.end(), service.c_str(), service.c_str() +
service.size() + 1);

result += std::to_string(results.size());
result.append(1, '\0');
for(auto& e : results)
{
result += e.endpoint().address().to_string();
result.append(1, '\0');
result += std::to_string(e.endpoint().port());
result.append(1, '\0');
}

return result;
}

/// Synchronous function executed in the child process to perform the
resolve,
/// serialise the resulting results obejct, and
/// send the serialised string to the write end of the pipe
void
do_child(asio::local::stream_protocol::socket sock, std::string hostname,
std::string service)
{
auto resolver = asio::ip::tcp::resolver(sock.get_executor());
auto results = resolver.resolve(hostname, service);
auto buf = serialise(results);
auto written = asio::write(sock, asio::buffer(buf));
sock.shutdown(asio::socket_base::shutdown_send);
}

/// Coroutine to asynchronously wait for the given child to exit
asio::awaitable< void >
wait_child(int childpid)
{
auto sigs = asio::signal_set(co_await asio::this_coro::executor,
SIGCHLD);
for (;;)
{
int state = 0;
auto ret = ::waitpid(childpid, &state, WNOHANG);
if (ret != 0)
break;
auto sig = co_await sigs.async_wait(asio::use_awaitable);
if (sig != SIGCHLD)
std::cout << "strange signal: " << sig << "\n";
}
}

/// Consume an individual string segment from the read end of the pipe.
asio::awaitable<std::string>
consume_token(asio::local::stream_protocol::socket& s, std::string&
rxbuffer)
{
auto size = co_await asio::async_read_until(s,
asio::dynamic_buffer(rxbuffer), '\0', asio::use_awaitable);
auto result = rxbuffer.substr(0, size - 1);
rxbuffer.erase(0, size);
co_return result;
}

/// Coroutine to deserialise the resolve results from the read end of the
socket.
asio::awaitable<std::vector<asio::ip::tcp::endpoint >>
collect_endpoints(asio::local::stream_protocol::socket& from_child)
{
std::vector<asio::ip::tcp::endpoint> endpoints;
std::string rxbuf;

// rxhost and rxservice unused for now
auto rxhost = co_await consume_token(from_child, rxbuf);
auto rxservice = co_await consume_token(from_child, rxbuf);

auto n_str = co_await consume_token(from_child, rxbuf);
auto entries = ::atoi(n_str.c_str());
endpoints.reserve(entries);
while(entries--)
{
auto addr = asio::ip::make_address(co_await
consume_token(from_child, rxbuf));
auto port = static_cast<unsigned short>(::atol((co_await
consume_token(from_child, rxbuf)).c_str()));
endpoints.push_back(asio::ip::tcp::endpoint(addr, port));
}
co_return endpoints;
}

/// Asyncronous resolve expressed in terms of a child process
asio::awaitable<std::vector<asio::ip::tcp::endpoint >>
resolve(std::string hostname, std::string service,
std::chrono::milliseconds timeout)
{
// make a pipe
asio::local::stream_protocol::socket
parent { co_await asio::this_coro::executor },
child { co_await asio::this_coro::executor };
asio::local::connect_pair(parent, child);

// query the current executor for the execution context
auto& ctx = asio::query(co_await asio::this_coro::executor,
asio::execution::context);

// notify the execution context that we are about to fork
ctx.notify_fork(asio::execution_context::fork_prepare);
auto childpid = ::fork();
if (childpid == 0)
{
// in the child we close the read end of the pipe and write to the
other end
ctx.notify_fork(asio::execution_context::fork_child);
parent.close();
do_child(std::move(child), hostname, service);
std::exit(0);
}
else if (childpid == -1)
{
// error case - failed to fork
auto ec = error_code(errno, generic_category());
ctx.notify_fork(asio::execution_context::fork_parent);
throw system_error(ec);
}

// parent - close the write end of the pipe
ctx.notify_fork(asio::execution_context::fork_parent);
child.close();

std::vector<asio::ip::tcp::endpoint> endpoints;
std::exception_ptr except = nullptr;
try
{
using namespace asioex::awaitable_operators;
auto timer = asio::steady_timer(co_await asio::this_coro::executor,
timeout);

// wait for either collect_endpoints or timeout
auto t0 = std::chrono::steady_clock::now();
auto which = co_await (
collect_endpoints(parent) ||
timer.async_wait(asio::use_awaitable));
auto t1 = std::chrono::steady_clock::now();

// whichever finishes first, kill the child
::kill(childpid, SIGTERM);

if (which.index() == 0)
endpoints = std::get<0>(std::move(which));
else {
throw system_error(asio::error::timed_out);
}
}
catch (std::exception &e)
{
// catch exception and place into an exception pointer
// because we need to call a coroutine in the cleanup
// and you can't call coroutines from exception handlers
except = std::current_exception();
}

// wait for the child to finish
co_await wait_child(childpid);

if (except)
std::rethrow_exception(except);

co_return endpoints;
}

asio::awaitable< void >
resolve_test()
{
using namespace std::literals;

auto hostname = "google.com";
auto service = "http";

auto timeouts = std::array { 1ms, 5'000ms, 20'000ms };

for(auto t : timeouts)
{
std::cout << "timeout: " << t.count() << "ms : ";
try
{
auto endpoints = co_await resolve(hostname, service, t);
const char* sep = "";
for(auto&& e : endpoints) {
std::cout << sep << e;
sep = ", ";
}
}
catch(std::exception& e)
{
std::cout << "exception " << e.what();
}
std::cout << "\n";
}
}

int
main()
{
auto ioc = asio::io_context();

asio::co_spawn(ioc, resolve_test(), asio::detached);

ioc.run();
}

If Chris K is watching, I'd value his critique. This is probably much more
convoluted than it needs to be.

sandeep m.v via Boost

unread,
Sep 6, 2021, 7:04:05 AM9/6/21
to bo...@lists.boost.org, sandeep m.v
Thank you for the response.

We build our application for multiple platforms, Windows, Linux, Android
and iOS.
If it is not possible to cancel, is there a way to close internal socket,
so that it wont callback resolve handler?
By that way, we can at least go ahead with our processing after our
internal timeout assuming callback never happen later.
Is that something possible?

On Wed, Aug 25, 2021 at 1:29 AM Richard Hodges via Boost <
bo...@lists.boost.org> wrote:

> On Linux async_resolve is currently implemented in terms of a spawned
> thread which calls a synchronous function. It has a 30s timeout and can’t
> be cancelled.
>
> On Tue, 24 Aug 2021 at 21:56, Vinnie Falco via Boost <
> bo...@lists.boost.org>
> wrote:
>
> > On Tue, Aug 24, 2021 at 12:09 PM sandeep m.v via Boost
> > <bo...@lists.boost.org> wrote:
> > > Can someone please help me with this?
> >
> > Are you by chance using Windows? What version?
> >
> > Thanks
> >
> > _______________________________________________
> > Unsubscribe & other changes:
> > http://lists.boost.org/mailman/listinfo.cgi/boost
> >
> --
> Richard Hodges
> hodg...@gmail.com
> office: +44 2032 898 513
> home: +376 861 195
> mobile: +376 380 212
>
> _______________________________________________
> Unsubscribe & other changes:
> http://lists.boost.org/mailman/listinfo.cgi/boost
>

--
Regards,
Sandeep
Mob# 9000707098

_______________________________________________

Reply all
Reply to author
Forward
0 new messages