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