xrpld
Loading...
Searching...
No Matches
ResolverAsio.cpp
1#include <xrpl/basics/ResolverAsio.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/Resolver.h>
5#include <xrpl/beast/net/IPAddressConversion.h>
6#include <xrpl/beast/net/IPEndpoint.h>
7#include <xrpl/beast/utility/Journal.h>
8#include <xrpl/beast/utility/instrumentation.h>
9
10#include <boost/asio/bind_executor.hpp>
11#include <boost/asio/dispatch.hpp>
12#include <boost/asio/error.hpp>
13#include <boost/asio/io_context.hpp>
14#include <boost/asio/ip/tcp.hpp>
15#include <boost/asio/post.hpp>
16#include <boost/asio/strand.hpp>
17#include <boost/system/detail/error_code.hpp>
18
19#include <algorithm>
20#include <atomic>
21#include <cctype>
22#include <condition_variable>
23#include <deque>
24#include <functional>
25#include <iterator>
26#include <locale>
27#include <memory>
28#include <mutex>
29#include <ranges>
30#include <string>
31#include <utility>
32#include <vector>
33
34namespace xrpl {
35
41template <class Derived>
43{
45 {
46 }
47
48public:
50 {
51 // Destroying the object with I/O pending? Not a clean exit!
52 XRPL_ASSERT(pending_.load() == 0, "xrpl::AsyncObject::~AsyncObject : nothing pending");
53 }
54
61 {
62 public:
63 explicit CompletionCounter(Derived* owner) : owner_(owner)
64 {
65 ++owner_->pending_;
66 }
67
69 {
70 ++owner_->pending_;
71 }
72
74 {
75 if (--owner_->pending_ == 0)
76 owner_->asyncHandlersComplete();
77 }
78
80 operator=(CompletionCounter const&) = delete;
81
82 private:
84 };
85
86 void
88 {
89 ++pending_;
90 }
91
92 void
94 {
95 if (--pending_ == 0)
96 (static_cast<Derived*>(this))->asyncHandlersComplete();
97 }
98
99private:
100 // The number of handlers pending.
102
103 friend Derived;
104};
105
106class ResolverAsioImpl : public ResolverAsio, public AsyncObject<ResolverAsioImpl>
107{
108public:
110
112
113 boost::asio::io_context& ioContext;
114 boost::asio::strand<boost::asio::io_context::executor_type> strand;
115 boost::asio::ip::tcp::resolver resolver;
116
120
123
124 // Represents a unit of work for the resolver to do
125 struct Work
126 {
129
130 template <class StringSequence>
131 Work(StringSequence const& inNames, HandlerType handler) : handler(std::move(handler))
132 {
133 names.reserve(inNames.size());
134
135 std::reverse_copy(inNames.begin(), inNames.end(), std::back_inserter(names));
136 }
137 };
138
140
144 , strand(boost::asio::make_strand(ioContext))
146 , stopCalled(false)
147 , stopped(true)
148 {
149 }
150
152 {
153 XRPL_ASSERT(work.empty(), "xrpl::ResolverAsioImpl::~ResolverAsioImpl : no pending work");
154 XRPL_ASSERT(stopped, "xrpl::ResolverAsioImpl::~ResolverAsioImpl : stopped");
155 }
156
157 //-------------------------------------------------------------------------
158 // AsyncObject
159 void
161 {
164 cv.notify_all();
165 }
166
167 //--------------------------------------------------------------------------
168 //
169 // Resolver
170 //
171 //--------------------------------------------------------------------------
172
173 void
174 start() override
175 {
176 XRPL_ASSERT(stopped == true, "xrpl::ResolverAsioImpl::start : stopped");
177 XRPL_ASSERT(stopCalled == false, "xrpl::ResolverAsioImpl::start : not stopping");
178
179 if (stopped.exchange(false))
180 {
181 {
182 std::scoped_lock const lk{mut};
184 }
185 addReference();
186 }
187 }
188
189 void
190 stopAsync() override
191 {
192 if (!stopCalled.exchange(true))
193 {
194 boost::asio::dispatch(
195 ioContext,
196 boost::asio::bind_executor(
197 strand, [this, counter = CompletionCounter(this)] { doStop(counter); }));
198
199 JLOG(journal.debug()) << "Queued a stop request";
200 }
201 }
202
203 void
204 stop() override
205 {
206 stopAsync();
207
208 JLOG(journal.debug()) << "Waiting to stop";
210 cv.wait(lk, [this] { return asyncHandlersCompleted; });
211 lk.unlock();
212 JLOG(journal.debug()) << "Stopped";
213 }
214
215 void
216 resolve(std::vector<std::string> const& names, HandlerType const& handler) override
217 {
218 XRPL_ASSERT(stopCalled == false, "xrpl::ResolverAsioImpl::resolve : not stopping");
219 XRPL_ASSERT(!names.empty(), "xrpl::ResolverAsioImpl::resolve : names non-empty");
220
221 // TODO NIKB use rvalue references to construct and move
222 // reducing cost.
223 boost::asio::dispatch(
224 ioContext,
225 boost::asio::bind_executor(
226 strand, [this, names, handler, counter = CompletionCounter(this)] {
227 doResolve(names, handler, counter);
228 }));
229 }
230
231 //-------------------------------------------------------------------------
232 // Resolver
233 void
234 doStop(CompletionCounter)
235 {
236 XRPL_ASSERT(stopCalled == true, "xrpl::ResolverAsioImpl::doStop : stopping");
237
238 if (!stopped.exchange(true))
239 {
240 work.clear();
241 resolver.cancel();
242
244 }
245 }
246
247 void
249 std::string name,
250 boost::system::error_code const& ec,
251 HandlerType handler,
252 boost::asio::ip::tcp::resolver::results_type results,
253 CompletionCounter)
254 {
255 if (ec == boost::asio::error::operation_aborted)
256 return;
257
259 auto iter = results.begin();
260
261 // If we get an error message back, we don't return any
262 // results that we may have gotten.
263 if (!ec)
264 {
265 while (iter != results.end())
266 {
268 ++iter;
269 }
270 }
271
272 handler(name, addresses);
273
274 boost::asio::post(
275 ioContext,
276 boost::asio::bind_executor(
277 strand, [this, counter = CompletionCounter(this)] { doWork(counter); }));
278 }
279
280 static HostAndPort
282 {
283 // first attempt to parse as an endpoint (IP addr + port).
284 // If that doesn't succeed, fall back to generic name + port parsing
285
286 if (auto const result = beast::ip::Endpoint::fromStringChecked(str))
287 {
288 return make_pair(result->address().to_string(), std::to_string(result->port()));
289 }
290
291 // generic name/port parsing, which doesn't work for
292 // IPv6 addresses in particular because it considers a colon
293 // a port separator
294
295 // Attempt to find the first and last non-whitespace
296 std::locale const loc;
297 auto const findWhitespace = [&loc](std::string::value_type c) {
298 return std::isspace<std::string::value_type>(c, loc);
299 };
300
301 auto hostFirst = std::ranges::find_if_not(str, findWhitespace);
302
303 auto portLast =
304 std::ranges::find_if_not(std::ranges::reverse_view(str), findWhitespace).base();
305
306 // This should only happen for all-whitespace strings
307 if (hostFirst >= portLast)
309
310 // Attempt to find the first and last valid port separators
311 auto const findPortSeparator = [](char const c) -> bool {
312 if (std::isspace(static_cast<unsigned char>(c)))
313 return true;
314
315 if (c == ':')
316 return true;
317
318 return false;
319 };
320
321 auto hostLast = std::find_if(hostFirst, portLast, findPortSeparator);
322
323 auto portFirst = std::find_if_not(hostLast, portLast, findPortSeparator);
324
325 return make_pair(std::string(hostFirst, hostLast), std::string(portFirst, portLast));
326 }
327
328 void
329 doWork(CompletionCounter)
330 {
331 if (stopCalled)
332 return;
333
334 // We don't have any work to do at this time
335 if (work.empty())
336 return;
337
338 std::string const name(work.front().names.back());
339 HandlerType const handler(work.front().handler);
340
341 work.front().names.pop_back();
342
343 if (work.front().names.empty())
344 work.pop_front();
345
346 auto const [host, port] = parseName(name);
347
348 if (host.empty())
349 {
350 JLOG(journal.error()) << "Unable to parse '" << name << "'";
351
352 boost::asio::post(
353 ioContext,
354 boost::asio::bind_executor(
355 strand, [this, counter = CompletionCounter(this)] { doWork(counter); }));
356
357 return;
358 }
359
360 resolver.async_resolve(
361 host,
362 port,
363 [this, name, handler, counter = CompletionCounter(this)](
364 boost::system::error_code const& ec,
365 boost::asio::ip::tcp::resolver::results_type results) {
366 doFinish(name, ec, handler, results, counter);
367 });
368 }
369
370 void
371 doResolve(std::vector<std::string> const& names, HandlerType const& handler, CompletionCounter)
372 {
373 XRPL_ASSERT(!names.empty(), "xrpl::ResolverAsioImpl::doResolve : names non-empty");
374
375 if (!stopCalled)
376 {
377 work.emplace_back(names, handler);
378
379 JLOG(journal.debug()) << "Queued new job with " << names.size() << " tasks. "
380 << work.size() << " jobs outstanding.";
381
382 if (!work.empty())
383 {
384 boost::asio::post(
385 ioContext,
386 boost::asio::bind_executor(
387 strand, [this, counter = CompletionCounter(this)] { doWork(counter); }));
388 }
389 }
390 }
391};
392
393//-----------------------------------------------------------------------------
394
396ResolverAsio::make(boost::asio::io_context& ioContext, beast::Journal journal)
397{
398 return std::make_unique<ResolverAsioImpl>(ioContext, journal);
399}
400
401//-----------------------------------------------------------------------------
402Resolver::~Resolver() = default;
403} // namespace xrpl
T back_inserter(T... args)
T begin(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
static std::optional< Endpoint > fromStringChecked(std::string const &s)
Create an Endpoint from a string.
RAII container that maintains the count of pending I/O.
CompletionCounter & operator=(CompletionCounter const &)=delete
CompletionCounter(CompletionCounter const &other)
std::atomic< int > pending_
void resolve(std::vector< std::string > const &names, HandlerType const &handler) override
void stopAsync() override
Issue an asynchronous stop request.
void start() override
Issue a synchronous start request.
ResolverAsioImpl(boost::asio::io_context &ioContext, beast::Journal journal)
std::condition_variable cv
std::atomic< bool > stopCalled
void doWork(CompletionCounter)
std::deque< Work > work
void stop() override
Issue a synchronous stop request.
static HostAndPort parseName(std::string const &str)
void doFinish(std::string name, boost::system::error_code const &ec, HandlerType handler, boost::asio::ip::tcp::resolver::results_type results, CompletionCounter)
boost::asio::ip::tcp::resolver resolver
boost::asio::strand< boost::asio::io_context::executor_type > strand
std::pair< std::string, std::string > HostAndPort
boost::asio::io_context & ioContext
std::atomic< bool > stopped
void doStop(CompletionCounter)
void doResolve(std::vector< std::string > const &names, HandlerType const &handler, CompletionCounter)
static std::unique_ptr< ResolverAsio > make(boost::asio::io_context &, beast::Journal)
ResolverAsio()=default
std::function< void(std::string, std::vector< beast::ip::Endpoint >)> HandlerType
Definition Resolver.h:14
virtual ~Resolver()=0
T empty(T... args)
T find_if_not(T... args)
T make_pair(T... args)
T make_unique(T... args)
STL namespace.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
T push_back(T... args)
T reverse_copy(T... args)
T size(T... args)
static ip::Endpoint fromAsio(boost::asio::ip::address const &address)
Work(StringSequence const &inNames, HandlerType handler)
std::vector< std::string > names
T to_string(T... args)
T unlock(T... args)