TLA Line data Source code
1 : //
2 : // Copyright (c) 2026 Steve Gerbino
3 : // Copyright (c) 2026 Michael Vandeberg
4 : //
5 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 : //
8 : // Official repository: https://github.com/cppalliance/corosio
9 : //
10 :
11 : #ifndef BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RESOLVER_SERVICE_HPP
12 : #define BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RESOLVER_SERVICE_HPP
13 :
14 : #include <boost/corosio/detail/platform.hpp>
15 :
16 : #if BOOST_COROSIO_POSIX
17 :
18 : #include <boost/corosio/native/detail/posix/posix_resolver.hpp>
19 : #include <boost/corosio/native/detail/reactor/reactor_scheduler.hpp>
20 : #include <boost/corosio/detail/thread_pool.hpp>
21 :
22 : #include <unordered_map>
23 :
24 : namespace boost::corosio::detail {
25 :
26 : /** Resolver service for POSIX backends.
27 :
28 : Owns all posix_resolver instances. Thread lifecycle is managed
29 : by the thread_pool service.
30 : */
31 : class BOOST_COROSIO_DECL posix_resolver_service final
32 : : public capy::execution_context::service
33 : , public io_object::io_service
34 : {
35 : public:
36 : using key_type = posix_resolver_service;
37 :
38 HIT 2241 : posix_resolver_service(capy::execution_context& ctx, scheduler& sched)
39 4482 : : sched_(&sched)
40 2241 : , pool_(ctx)
41 : {
42 2241 : }
43 :
44 4482 : ~posix_resolver_service() override = default;
45 :
46 : posix_resolver_service(posix_resolver_service const&) = delete;
47 : posix_resolver_service& operator=(posix_resolver_service const&) = delete;
48 :
49 : io_object::implementation* construct() override;
50 :
51 63 : void destroy(io_object::implementation* p) override
52 : {
53 63 : auto& impl = static_cast<posix_resolver&>(*p);
54 63 : impl.cancel();
55 63 : destroy_impl(impl);
56 63 : }
57 :
58 : void shutdown() override;
59 : void destroy_impl(posix_resolver& impl);
60 :
61 : void post(scheduler_op* op);
62 :
63 : /** Return the resolver thread pool.
64 :
65 : The pool's service is created on first use, so this can fail
66 : where a plain accessor could not. Its workers start later, on
67 : the first post, and a thread the system refuses there is
68 : reported by that post rather than thrown here.
69 :
70 : @throws std::bad_alloc If the service cannot be allocated.
71 :
72 : @return The context's shared blocking-I/O pool.
73 :
74 : @see thread_pool_ref::get
75 : */
76 52 : thread_pool& pool()
77 : {
78 52 : return pool_.get();
79 : }
80 :
81 : /// True when the resolver thread pool is unavailable: the `unsafe` tier,
82 : /// whose lockless scheduler cannot accept the pool's cross-thread
83 : /// completions.
84 54 : bool resolver_unavailable() const noexcept
85 : {
86 54 : return sched_->scheduler_locking_disabled();
87 : }
88 :
89 : private:
90 : scheduler* sched_;
91 : thread_pool_ref pool_;
92 : std::mutex mutex_;
93 : intrusive_list<posix_resolver> resolver_list_;
94 : std::unordered_map<posix_resolver*, std::shared_ptr<posix_resolver>>
95 : resolver_ptrs_;
96 : };
97 :
98 : /** Get or create the resolver service for the given context.
99 :
100 : This function is called by the concrete scheduler during initialization
101 : to create the resolver service with a reference to itself.
102 :
103 : @param ctx Reference to the owning execution_context.
104 : @param sched Reference to the scheduler for posting completions.
105 : @return Reference to the resolver service.
106 : */
107 : posix_resolver_service&
108 : get_resolver_service(capy::execution_context& ctx, scheduler& sched);
109 :
110 : // ---------------------------------------------------------------------------
111 : // Inline implementation
112 : // ---------------------------------------------------------------------------
113 :
114 : // posix_resolver_detail helpers
115 :
116 : inline int
117 33 : posix_resolver_detail::flags_to_hints(resolve_flags flags)
118 : {
119 33 : int hints = 0;
120 :
121 33 : if ((flags & resolve_flags::passive) != resolve_flags::none)
122 1 : hints |= AI_PASSIVE;
123 33 : if ((flags & resolve_flags::numeric_host) != resolve_flags::none)
124 18 : hints |= AI_NUMERICHOST;
125 33 : if ((flags & resolve_flags::numeric_service) != resolve_flags::none)
126 12 : hints |= AI_NUMERICSERV;
127 33 : if ((flags & resolve_flags::address_configured) != resolve_flags::none)
128 1 : hints |= AI_ADDRCONFIG;
129 33 : if ((flags & resolve_flags::v4_mapped) != resolve_flags::none)
130 1 : hints |= AI_V4MAPPED;
131 33 : if ((flags & resolve_flags::all_matching) != resolve_flags::none)
132 1 : hints |= AI_ALL;
133 :
134 33 : return hints;
135 : }
136 :
137 : inline int
138 17 : posix_resolver_detail::flags_to_ni_flags(reverse_flags flags)
139 : {
140 17 : int ni_flags = 0;
141 :
142 17 : if ((flags & reverse_flags::numeric_host) != reverse_flags::none)
143 7 : ni_flags |= NI_NUMERICHOST;
144 17 : if ((flags & reverse_flags::numeric_service) != reverse_flags::none)
145 7 : ni_flags |= NI_NUMERICSERV;
146 17 : if ((flags & reverse_flags::name_required) != reverse_flags::none)
147 1 : ni_flags |= NI_NAMEREQD;
148 17 : if ((flags & reverse_flags::datagram_service) != reverse_flags::none)
149 1 : ni_flags |= NI_DGRAM;
150 :
151 17 : return ni_flags;
152 : }
153 :
154 : inline std::vector<endpoint>
155 21 : posix_resolver_detail::convert_results(struct addrinfo* ai)
156 : {
157 21 : std::vector<endpoint> endpoints;
158 21 : endpoints.reserve(4); // Most lookups return 1-4 addresses
159 :
160 42 : for (auto* p = ai; p != nullptr; p = p->ai_next)
161 : {
162 21 : if (p->ai_family == AF_INET)
163 : {
164 18 : auto* addr = reinterpret_cast<sockaddr_in*>(p->ai_addr);
165 18 : endpoints.push_back(from_sockaddr_in(*addr));
166 : }
167 3 : else if (p->ai_family == AF_INET6)
168 : {
169 3 : auto* addr = reinterpret_cast<sockaddr_in6*>(p->ai_addr);
170 3 : endpoints.push_back(from_sockaddr_in6(*addr));
171 : }
172 : }
173 :
174 21 : return endpoints;
175 MIS 0 : }
176 :
177 : inline std::error_code
178 HIT 26 : posix_resolver_detail::make_gai_error(int gai_err)
179 : {
180 : // Map GAI errors to appropriate generic error codes
181 26 : switch (gai_err)
182 : {
183 1 : case EAI_AGAIN:
184 : // Temporary failure - try again later
185 1 : return std::error_code(
186 : static_cast<int>(std::errc::resource_unavailable_try_again),
187 1 : std::generic_category());
188 :
189 1 : case EAI_BADFLAGS:
190 : // Invalid flags
191 1 : return std::error_code(
192 : static_cast<int>(std::errc::invalid_argument),
193 1 : std::generic_category());
194 :
195 11 : case EAI_FAIL:
196 : // Non-recoverable failure
197 11 : return std::error_code(
198 11 : static_cast<int>(std::errc::io_error), std::generic_category());
199 :
200 1 : case EAI_FAMILY:
201 : // Address family not supported
202 1 : return std::error_code(
203 : static_cast<int>(std::errc::address_family_not_supported),
204 1 : std::generic_category());
205 :
206 1 : case EAI_MEMORY:
207 : // Memory allocation failure
208 1 : return std::error_code(
209 : static_cast<int>(std::errc::not_enough_memory),
210 1 : std::generic_category());
211 :
212 7 : case EAI_NONAME:
213 : // Host or service not found
214 7 : return std::error_code(
215 : static_cast<int>(std::errc::no_such_device_or_address),
216 7 : std::generic_category());
217 :
218 1 : case EAI_SERVICE:
219 : // Service not supported for socket type
220 1 : return std::error_code(
221 : static_cast<int>(std::errc::invalid_argument),
222 1 : std::generic_category());
223 :
224 1 : case EAI_SOCKTYPE:
225 : // Socket type not supported
226 1 : return std::error_code(
227 : static_cast<int>(std::errc::not_supported),
228 1 : std::generic_category());
229 :
230 1 : case EAI_SYSTEM:
231 : // System error - use errno
232 1 : return std::error_code(errno, std::generic_category());
233 :
234 1 : default:
235 : // Unknown error
236 1 : return std::error_code(
237 1 : static_cast<int>(std::errc::io_error), std::generic_category());
238 : }
239 : }
240 :
241 : // posix_resolver
242 :
243 64 : inline posix_resolver::posix_resolver(posix_resolver_service& svc) noexcept
244 64 : : svc_(svc)
245 : {
246 64 : }
247 :
248 : // posix_resolver::resolve_op implementation
249 :
250 : inline void
251 34 : posix_resolver::resolve_op::reset() noexcept
252 : {
253 34 : host.clear();
254 34 : service.clear();
255 34 : flags = resolve_flags::none;
256 34 : stored_results = std::vector<endpoint>{};
257 34 : gai_error = 0;
258 34 : cancelled.store(false, std::memory_order_relaxed);
259 34 : stop_cb.reset();
260 34 : ec_out = nullptr;
261 34 : out = nullptr;
262 34 : }
263 :
264 : inline void
265 32 : posix_resolver::resolve_op::operator()()
266 : {
267 32 : stop_cb.reset(); // Disconnect stop callback
268 :
269 32 : bool const was_cancelled = cancelled.load(std::memory_order_acquire);
270 :
271 32 : if (ec_out)
272 : {
273 32 : if (was_cancelled)
274 MIS 0 : *ec_out = capy::error::canceled;
275 HIT 32 : else if (gai_error != 0)
276 11 : *ec_out = posix_resolver_detail::make_gai_error(gai_error);
277 : else
278 21 : *ec_out = {}; // Clear on success
279 : }
280 :
281 32 : if (out && !was_cancelled && gai_error == 0)
282 21 : *out = std::move(stored_results);
283 :
284 : // Hold the keepalive across the dispatch: it may be the last
285 : // reference to the implementation this op is embedded in.
286 32 : auto prevent_destroy = std::move(impl_ptr);
287 32 : ex.on_work_finished();
288 32 : cont.h = h;
289 32 : dispatch_coro(ex, cont).resume();
290 32 : }
291 :
292 : inline void
293 1 : posix_resolver::resolve_op::destroy()
294 : {
295 1 : stop_cb.reset();
296 1 : auto local_ex = ex;
297 : // May destroy the implementation, and with it this op.
298 1 : impl_ptr.reset();
299 1 : local_ex.on_work_finished();
300 1 : }
301 :
302 : // posix_resolver::reverse_resolve_op implementation
303 :
304 : inline void
305 18 : posix_resolver::reverse_resolve_op::reset() noexcept
306 : {
307 18 : ep = endpoint{};
308 18 : flags = reverse_flags::none;
309 18 : stored_host.clear();
310 18 : stored_service.clear();
311 18 : gai_error = 0;
312 18 : cancelled.store(false, std::memory_order_relaxed);
313 18 : stop_cb.reset();
314 18 : ec_out = nullptr;
315 18 : result_out = nullptr;
316 18 : }
317 :
318 : inline void
319 16 : posix_resolver::reverse_resolve_op::operator()()
320 : {
321 16 : stop_cb.reset(); // Disconnect stop callback
322 :
323 16 : bool const was_cancelled = cancelled.load(std::memory_order_acquire);
324 :
325 16 : if (ec_out)
326 : {
327 16 : if (was_cancelled)
328 MIS 0 : *ec_out = capy::error::canceled;
329 HIT 16 : else if (gai_error != 0)
330 6 : *ec_out = posix_resolver_detail::make_gai_error(gai_error);
331 : else
332 10 : *ec_out = {}; // Clear on success
333 : }
334 :
335 16 : if (result_out && !was_cancelled && gai_error == 0)
336 : {
337 10 : *result_out =
338 10 : endpoint_name{std::move(stored_host), std::move(stored_service)};
339 : }
340 :
341 : // Hold the keepalive across the dispatch: it may be the last
342 : // reference to the implementation this op is embedded in.
343 16 : auto prevent_destroy = std::move(impl_ptr);
344 16 : ex.on_work_finished();
345 16 : cont.h = h;
346 16 : dispatch_coro(ex, cont).resume();
347 16 : }
348 :
349 : inline void
350 1 : posix_resolver::reverse_resolve_op::destroy()
351 : {
352 1 : stop_cb.reset();
353 1 : auto local_ex = ex;
354 : // May destroy the implementation, and with it this op.
355 1 : impl_ptr.reset();
356 1 : local_ex.on_work_finished();
357 1 : }
358 :
359 : // posix_resolver implementation
360 :
361 : inline std::coroutine_handle<>
362 35 : posix_resolver::resolve(
363 : std::coroutine_handle<> h,
364 : capy::executor_ref ex,
365 : std::string_view host,
366 : std::string_view service,
367 : resolve_flags flags,
368 : std::stop_token token,
369 : std::error_code* ec,
370 : std::vector<endpoint>* out)
371 : {
372 35 : if (svc_.resolver_unavailable())
373 : {
374 1 : *ec = std::make_error_code(std::errc::operation_not_supported);
375 1 : op_.cont.h = h;
376 1 : return dispatch_coro(ex, op_.cont);
377 : }
378 :
379 34 : auto& op = op_;
380 34 : op.reset();
381 34 : op.h = h;
382 34 : op.ex = ex;
383 34 : op.ec_out = ec;
384 34 : op.out = out;
385 34 : op.host = host;
386 34 : op.service = service;
387 34 : op.flags = flags;
388 34 : op.start(token);
389 :
390 : // Keep io_context alive while resolution is pending
391 34 : op.ex.on_work_started();
392 :
393 : // Prevent impl destruction while work is in flight
394 34 : resolve_pool_op_.resolver_ = this;
395 34 : resolve_pool_op_.ref_ = this->shared_from_this();
396 34 : resolve_pool_op_.func_ = &posix_resolver::do_resolve_work;
397 34 : if (auto pec = svc_.pool().post(&resolve_pool_op_))
398 : {
399 : // The pool is shutting down, or the system refused it a thread.
400 : // Nothing of this resolve went cross-thread, so it answers here
401 : // like the no-resolver exit above rather than through a
402 : // completion the scheduler has to carry back.
403 1 : resolve_pool_op_.ref_.reset();
404 1 : op.stop_cb.reset();
405 1 : op.ex.on_work_finished();
406 1 : *ec = pec;
407 1 : op.cont.h = h;
408 1 : return dispatch_coro(ex, op.cont);
409 : }
410 33 : return std::noop_coroutine();
411 : }
412 :
413 : inline std::coroutine_handle<>
414 19 : posix_resolver::reverse_resolve(
415 : std::coroutine_handle<> h,
416 : capy::executor_ref ex,
417 : endpoint const& ep,
418 : reverse_flags flags,
419 : std::stop_token token,
420 : std::error_code* ec,
421 : endpoint_name* result_out)
422 : {
423 19 : if (svc_.resolver_unavailable())
424 : {
425 1 : *ec = std::make_error_code(std::errc::operation_not_supported);
426 1 : reverse_op_.cont.h = h;
427 1 : return dispatch_coro(ex, reverse_op_.cont);
428 : }
429 :
430 18 : auto& op = reverse_op_;
431 18 : op.reset();
432 18 : op.h = h;
433 18 : op.ex = ex;
434 18 : op.ec_out = ec;
435 18 : op.result_out = result_out;
436 18 : op.ep = ep;
437 18 : op.flags = flags;
438 18 : op.start(token);
439 :
440 : // Keep io_context alive while resolution is pending
441 18 : op.ex.on_work_started();
442 :
443 : // Prevent impl destruction while work is in flight
444 18 : reverse_pool_op_.resolver_ = this;
445 18 : reverse_pool_op_.ref_ = this->shared_from_this();
446 18 : reverse_pool_op_.func_ = &posix_resolver::do_reverse_resolve_work;
447 18 : if (auto pec = svc_.pool().post(&reverse_pool_op_))
448 : {
449 : // The pool is shutting down, or the system refused it a thread.
450 : // Nothing of this resolve went cross-thread, so it answers here
451 : // like the no-resolver exit above rather than through a
452 : // completion the scheduler has to carry back.
453 1 : reverse_pool_op_.ref_.reset();
454 1 : op.stop_cb.reset();
455 1 : op.ex.on_work_finished();
456 1 : *ec = pec;
457 1 : op.cont.h = h;
458 1 : return dispatch_coro(ex, op.cont);
459 : }
460 17 : return std::noop_coroutine();
461 : }
462 :
463 : inline void
464 71 : posix_resolver::cancel() noexcept
465 : {
466 71 : op_.request_cancel();
467 71 : reverse_op_.request_cancel();
468 71 : }
469 :
470 : inline void
471 33 : posix_resolver::do_resolve_work(pool_work_item* w) noexcept
472 : {
473 33 : auto* pw = static_cast<pool_op*>(w);
474 33 : auto* self = pw->resolver_;
475 :
476 33 : struct addrinfo hints{};
477 33 : hints.ai_family = AF_UNSPEC;
478 33 : hints.ai_socktype = SOCK_STREAM;
479 33 : hints.ai_flags = posix_resolver_detail::flags_to_hints(self->op_.flags);
480 :
481 33 : struct addrinfo* ai = nullptr;
482 99 : int result = ::getaddrinfo(
483 66 : self->op_.host.empty() ? nullptr : self->op_.host.c_str(),
484 61 : self->op_.service.empty() ? nullptr : self->op_.service.c_str(), &hints,
485 : &ai);
486 :
487 33 : if (!self->op_.cancelled.load(std::memory_order_acquire))
488 : {
489 32 : if (result == 0 && ai)
490 : {
491 : self->op_.stored_results =
492 21 : posix_resolver_detail::convert_results(ai);
493 21 : self->op_.gai_error = 0;
494 : }
495 : else
496 : {
497 11 : self->op_.gai_error = result;
498 : }
499 : }
500 :
501 33 : if (ai)
502 22 : ::freeaddrinfo(ai);
503 :
504 : // Hand the keepalive to the op: the completion waits in the
505 : // scheduler's queue, and the implementation embedding it must
506 : // outlive that wait. Nothing may touch *self after the post.
507 33 : self->op_.impl_ptr = std::move(pw->ref_);
508 33 : self->svc_.post(&self->op_);
509 33 : }
510 :
511 : inline void
512 17 : posix_resolver::do_reverse_resolve_work(pool_work_item* w) noexcept
513 : {
514 17 : auto* pw = static_cast<pool_op*>(w);
515 17 : auto* self = pw->resolver_;
516 :
517 17 : sockaddr_storage ss{};
518 : socklen_t ss_len;
519 :
520 17 : if (self->reverse_op_.ep.is_v4())
521 : {
522 15 : auto sa = to_sockaddr_in(self->reverse_op_.ep);
523 15 : std::memcpy(&ss, &sa, sizeof(sa));
524 15 : ss_len = sizeof(sockaddr_in);
525 : }
526 : else
527 : {
528 2 : auto sa = to_sockaddr_in6(self->reverse_op_.ep);
529 2 : std::memcpy(&ss, &sa, sizeof(sa));
530 2 : ss_len = sizeof(sockaddr_in6);
531 : }
532 :
533 : char host[NI_MAXHOST];
534 : char service[NI_MAXSERV];
535 :
536 17 : int result = ::getnameinfo(
537 : reinterpret_cast<sockaddr*>(&ss), ss_len, host, sizeof(host), service,
538 : sizeof(service),
539 : posix_resolver_detail::flags_to_ni_flags(self->reverse_op_.flags));
540 :
541 17 : if (!self->reverse_op_.cancelled.load(std::memory_order_acquire))
542 : {
543 16 : if (result == 0)
544 : {
545 10 : self->reverse_op_.stored_host = host;
546 10 : self->reverse_op_.stored_service = service;
547 10 : self->reverse_op_.gai_error = 0;
548 : }
549 : else
550 : {
551 6 : self->reverse_op_.gai_error = result;
552 : }
553 : }
554 :
555 : // Hand the keepalive to the op: the completion waits in the
556 : // scheduler's queue, and the implementation embedding it must
557 : // outlive that wait. Nothing may touch *self after the post.
558 17 : self->reverse_op_.impl_ptr = std::move(pw->ref_);
559 17 : self->svc_.post(&self->reverse_op_);
560 17 : }
561 :
562 : // posix_resolver_service implementation
563 :
564 : inline void
565 2241 : posix_resolver_service::shutdown()
566 : {
567 2241 : std::lock_guard<std::mutex> lock(mutex_);
568 :
569 : // Cancel all resolvers (sets cancelled flag checked by pool threads)
570 2242 : for (auto* impl = resolver_list_.pop_front(); impl != nullptr;
571 1 : impl = resolver_list_.pop_front())
572 : {
573 1 : impl->cancel();
574 : }
575 :
576 : // Clear the map which releases shared_ptrs.
577 : // The thread pool service shuts down separately via
578 : // execution_context service ordering.
579 2241 : resolver_ptrs_.clear();
580 2241 : }
581 :
582 : inline io_object::implementation*
583 64 : posix_resolver_service::construct()
584 : {
585 64 : auto ptr = std::make_shared<posix_resolver>(*this);
586 64 : auto* impl = ptr.get();
587 :
588 : {
589 64 : std::lock_guard<std::mutex> lock(mutex_);
590 64 : resolver_list_.push_back(impl);
591 64 : resolver_ptrs_[impl] = std::move(ptr);
592 64 : }
593 :
594 64 : return impl;
595 64 : }
596 :
597 : inline void
598 63 : posix_resolver_service::destroy_impl(posix_resolver& impl)
599 : {
600 63 : std::lock_guard<std::mutex> lock(mutex_);
601 63 : resolver_list_.remove(&impl);
602 63 : resolver_ptrs_.erase(&impl);
603 63 : }
604 :
605 : inline void
606 50 : posix_resolver_service::post(scheduler_op* op)
607 : {
608 50 : sched_->post(op);
609 50 : }
610 :
611 : // Free function to get/create the resolver service
612 :
613 : inline posix_resolver_service&
614 2241 : get_resolver_service(capy::execution_context& ctx, scheduler& sched)
615 : {
616 2241 : return ctx.make_service<posix_resolver_service>(sched);
617 : }
618 :
619 : } // namespace boost::corosio::detail
620 :
621 : #endif // BOOST_COROSIO_POSIX
622 :
623 : #endif // BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RESOLVER_SERVICE_HPP
|