include/boost/corosio/native/detail/posix/posix_resolver.hpp
25.0% Lines (2/8)
50.0% Functions (2/4)
include/boost/corosio/native/detail/posix/posix_resolver.hpp
| Line | Hits | Source Code |
|---|---|---|
| 1 | // | |
| 2 | // Copyright (c) 2026 Steve Gerbino | |
| 3 | // | |
| 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying | |
| 5 | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | |
| 6 | // | |
| 7 | // Official repository: https://github.com/cppalliance/corosio | |
| 8 | // | |
| 9 | ||
| 10 | #ifndef BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RESOLVER_HPP | |
| 11 | #define BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RESOLVER_HPP | |
| 12 | ||
| 13 | #include <boost/corosio/detail/platform.hpp> | |
| 14 | ||
| 15 | #if BOOST_COROSIO_POSIX | |
| 16 | ||
| 17 | #include <boost/corosio/detail/config.hpp> | |
| 18 | #include <boost/corosio/resolver.hpp> | |
| 19 | #include <boost/capy/ex/execution_context.hpp> | |
| 20 | ||
| 21 | #include <boost/corosio/detail/endpoint_convert.hpp> | |
| 22 | #include <boost/corosio/detail/intrusive.hpp> | |
| 23 | #include <boost/corosio/detail/dispatch_coro.hpp> | |
| 24 | #include <boost/corosio/detail/scheduler_op.hpp> | |
| 25 | ||
| 26 | #include <boost/corosio/detail/scheduler.hpp> | |
| 27 | #include <boost/corosio/resolver_results.hpp> | |
| 28 | #include <boost/capy/ex/executor_ref.hpp> | |
| 29 | #include <coroutine> | |
| 30 | #include <boost/capy/error.hpp> | |
| 31 | ||
| 32 | #include <netdb.h> | |
| 33 | #include <netinet/in.h> | |
| 34 | #include <sys/socket.h> | |
| 35 | ||
| 36 | #include <atomic> | |
| 37 | #include <cassert> | |
| 38 | #include <condition_variable> | |
| 39 | #include <cstring> | |
| 40 | #include <memory> | |
| 41 | #include <mutex> | |
| 42 | #include <optional> | |
| 43 | #include <stop_token> | |
| 44 | #include <string> | |
| 45 | #include <thread> | |
| 46 | #include <unordered_map> | |
| 47 | #include <vector> | |
| 48 | ||
| 49 | /* | |
| 50 | POSIX Resolver Service | |
| 51 | ====================== | |
| 52 | ||
| 53 | POSIX getaddrinfo() is a blocking call that cannot be monitored with | |
| 54 | epoll/kqueue/io_uring. We use a worker thread approach: each resolution | |
| 55 | spawns a dedicated thread that runs the blocking call and posts completion | |
| 56 | back to the scheduler. | |
| 57 | ||
| 58 | Thread-per-resolution Design | |
| 59 | ---------------------------- | |
| 60 | Simple, no thread pool complexity. DNS lookups are infrequent enough that | |
| 61 | thread creation overhead is acceptable. Detached threads self-manage; | |
| 62 | shared_ptr capture keeps impl alive until completion. | |
| 63 | ||
| 64 | Cancellation | |
| 65 | ------------ | |
| 66 | getaddrinfo() cannot be interrupted mid-call. We use an atomic flag to | |
| 67 | indicate cancellation was requested. The worker thread checks this flag | |
| 68 | after getaddrinfo() returns and reports the appropriate error. | |
| 69 | ||
| 70 | Class Hierarchy | |
| 71 | --------------- | |
| 72 | - posix_resolver_service (execution_context service, one per context) | |
| 73 | - Owns all posix_resolver instances via shared_ptr | |
| 74 | - Stores scheduler* for posting completions | |
| 75 | - posix_resolver (one per resolver object) | |
| 76 | - Contains embedded resolve_op and reverse_resolve_op for reuse | |
| 77 | - Uses shared_from_this to prevent premature destruction | |
| 78 | - resolve_op (forward resolution state) | |
| 79 | - Uses getaddrinfo() to resolve host/service to endpoints | |
| 80 | - reverse_resolve_op (reverse resolution state) | |
| 81 | - Uses getnameinfo() to resolve endpoint to host/service | |
| 82 | ||
| 83 | Worker Thread Lifetime | |
| 84 | ---------------------- | |
| 85 | Each resolve() spawns a detached thread. The thread captures a shared_ptr | |
| 86 | to posix_resolver, ensuring the impl (and its embedded op_) stays | |
| 87 | alive until the thread completes, even if the resolver is destroyed. | |
| 88 | ||
| 89 | Completion Flow | |
| 90 | --------------- | |
| 91 | Forward resolution: | |
| 92 | 1. resolve() sets up op_, spawns worker thread | |
| 93 | 2. Worker runs getaddrinfo() (blocking) | |
| 94 | 3. Worker stores results in op_.stored_results | |
| 95 | 4. Worker calls svc_.post(&op_) to queue completion | |
| 96 | 5. Scheduler invokes op_() which resumes the coroutine | |
| 97 | ||
| 98 | Reverse resolution follows the same pattern using getnameinfo(). | |
| 99 | ||
| 100 | Single-Inflight Constraint | |
| 101 | -------------------------- | |
| 102 | Each resolver has ONE embedded op_ for forward and ONE reverse_op_ for | |
| 103 | reverse resolution. Concurrent operations of the same type on the same | |
| 104 | resolver would corrupt state. Users must serialize operations per-resolver. | |
| 105 | ||
| 106 | Shutdown Synchronization | |
| 107 | ------------------------ | |
| 108 | The service tracks active worker threads via thread_started()/thread_finished(). | |
| 109 | During shutdown(), the service sets shutting_down_ flag and waits for all | |
| 110 | threads to complete before destroying resources. | |
| 111 | */ | |
| 112 | ||
| 113 | namespace boost::corosio::detail { | |
| 114 | ||
| 115 | struct scheduler; | |
| 116 | ||
| 117 | namespace posix_resolver_detail { | |
| 118 | ||
| 119 | // Convert resolve_flags to addrinfo ai_flags | |
| 120 | int flags_to_hints(resolve_flags flags); | |
| 121 | ||
| 122 | // Convert reverse_flags to getnameinfo NI_* flags | |
| 123 | int flags_to_ni_flags(reverse_flags flags); | |
| 124 | ||
| 125 | // Convert addrinfo results to resolver_results | |
| 126 | resolver_results convert_results( | |
| 127 | struct addrinfo* ai, std::string_view host, std::string_view service); | |
| 128 | ||
| 129 | // Convert getaddrinfo error codes to std::error_code | |
| 130 | std::error_code make_gai_error(int gai_err); | |
| 131 | ||
| 132 | } // namespace posix_resolver_detail | |
| 133 | ||
| 134 | class posix_resolver_service; | |
| 135 | ||
| 136 | /** Resolver implementation for POSIX backends. | |
| 137 | ||
| 138 | Each resolver instance contains a single embedded operation object (op_) | |
| 139 | that is reused for each resolve() call. This design avoids per-operation | |
| 140 | heap allocation but imposes a critical constraint: | |
| 141 | ||
| 142 | @par Single-Inflight Contract | |
| 143 | ||
| 144 | Only ONE resolve operation may be in progress at a time per resolver | |
| 145 | instance. Calling resolve() while a previous resolve() is still pending | |
| 146 | results in undefined behavior: | |
| 147 | ||
| 148 | - The new call overwrites op_ fields (host, service, coroutine handle) | |
| 149 | - The worker thread from the first call reads corrupted state | |
| 150 | - The wrong coroutine may be resumed, or resumed multiple times | |
| 151 | - Data races occur on non-atomic op_ members | |
| 152 | ||
| 153 | @par Safe Usage Patterns | |
| 154 | ||
| 155 | @code | |
| 156 | // CORRECT: Sequential resolves | |
| 157 | auto [ec1, r1] = co_await resolver.resolve("host1", "80"); | |
| 158 | auto [ec2, r2] = co_await resolver.resolve("host2", "80"); | |
| 159 | ||
| 160 | // CORRECT: Parallel resolves with separate resolver instances | |
| 161 | resolver r1(ctx), r2(ctx); | |
| 162 | auto [ec1, res1] = co_await r1.resolve("host1", "80"); // in one coroutine | |
| 163 | auto [ec2, res2] = co_await r2.resolve("host2", "80"); // in another | |
| 164 | ||
| 165 | // WRONG: Concurrent resolves on same resolver | |
| 166 | // These may run concurrently if launched in parallel - UNDEFINED BEHAVIOR | |
| 167 | auto f1 = resolver.resolve("host1", "80"); | |
| 168 | auto f2 = resolver.resolve("host2", "80"); // BAD: overlaps with f1 | |
| 169 | @endcode | |
| 170 | ||
| 171 | @par Thread Safety | |
| 172 | Distinct objects: Safe. | |
| 173 | Shared objects: Unsafe. See single-inflight contract above. | |
| 174 | */ | |
| 175 | class posix_resolver final | |
| 176 | : public resolver::implementation | |
| 177 | , public std::enable_shared_from_this<posix_resolver> | |
| 178 | , public intrusive_list<posix_resolver>::node | |
| 179 | { | |
| 180 | friend class posix_resolver_service; | |
| 181 | ||
| 182 | public: | |
| 183 | // resolve_op - operation state for a single DNS resolution | |
| 184 | ||
| 185 | struct resolve_op : scheduler_op | |
| 186 | { | |
| 187 | struct canceller | |
| 188 | { | |
| 189 | resolve_op* op; | |
| 190 | ✗ | void operator()() const noexcept |
| 191 | { | |
| 192 | ✗ | op->request_cancel(); |
| 193 | ✗ | } |
| 194 | }; | |
| 195 | ||
| 196 | // Coroutine state | |
| 197 | std::coroutine_handle<> h; | |
| 198 | capy::executor_ref ex; | |
| 199 | posix_resolver* impl = nullptr; | |
| 200 | ||
| 201 | // Output parameters | |
| 202 | std::error_code* ec_out = nullptr; | |
| 203 | resolver_results* out = nullptr; | |
| 204 | ||
| 205 | // Input parameters (owned copies for thread safety) | |
| 206 | std::string host; | |
| 207 | std::string service; | |
| 208 | resolve_flags flags = resolve_flags::none; | |
| 209 | ||
| 210 | // Result storage (populated by worker thread) | |
| 211 | resolver_results stored_results; | |
| 212 | int gai_error = 0; | |
| 213 | ||
| 214 | // Thread coordination | |
| 215 | std::atomic<bool> cancelled{false}; | |
| 216 | std::optional<std::stop_callback<canceller>> stop_cb; | |
| 217 | ||
| 218 | 29 | resolve_op() = default; |
| 219 | ||
| 220 | void reset() noexcept; | |
| 221 | void operator()() override; | |
| 222 | void destroy() override; | |
| 223 | void request_cancel() noexcept; | |
| 224 | void start(std::stop_token token); | |
| 225 | }; | |
| 226 | ||
| 227 | // reverse_resolve_op - operation state for reverse DNS resolution | |
| 228 | ||
| 229 | struct reverse_resolve_op : scheduler_op | |
| 230 | { | |
| 231 | struct canceller | |
| 232 | { | |
| 233 | reverse_resolve_op* op; | |
| 234 | ✗ | void operator()() const noexcept |
| 235 | { | |
| 236 | ✗ | op->request_cancel(); |
| 237 | ✗ | } |
| 238 | }; | |
| 239 | ||
| 240 | // Coroutine state | |
| 241 | std::coroutine_handle<> h; | |
| 242 | capy::executor_ref ex; | |
| 243 | posix_resolver* impl = nullptr; | |
| 244 | ||
| 245 | // Output parameters | |
| 246 | std::error_code* ec_out = nullptr; | |
| 247 | reverse_resolver_result* result_out = nullptr; | |
| 248 | ||
| 249 | // Input parameters | |
| 250 | endpoint ep; | |
| 251 | reverse_flags flags = reverse_flags::none; | |
| 252 | ||
| 253 | // Result storage (populated by worker thread) | |
| 254 | std::string stored_host; | |
| 255 | std::string stored_service; | |
| 256 | int gai_error = 0; | |
| 257 | ||
| 258 | // Thread coordination | |
| 259 | std::atomic<bool> cancelled{false}; | |
| 260 | std::optional<std::stop_callback<canceller>> stop_cb; | |
| 261 | ||
| 262 | 29 | reverse_resolve_op() = default; |
| 263 | ||
| 264 | void reset() noexcept; | |
| 265 | void operator()() override; | |
| 266 | void destroy() override; | |
| 267 | void request_cancel() noexcept; | |
| 268 | void start(std::stop_token token); | |
| 269 | }; | |
| 270 | ||
| 271 | explicit posix_resolver(posix_resolver_service& svc) noexcept; | |
| 272 | ||
| 273 | std::coroutine_handle<> resolve( | |
| 274 | std::coroutine_handle<>, | |
| 275 | capy::executor_ref, | |
| 276 | std::string_view host, | |
| 277 | std::string_view service, | |
| 278 | resolve_flags flags, | |
| 279 | std::stop_token, | |
| 280 | std::error_code*, | |
| 281 | resolver_results*) override; | |
| 282 | ||
| 283 | std::coroutine_handle<> reverse_resolve( | |
| 284 | std::coroutine_handle<>, | |
| 285 | capy::executor_ref, | |
| 286 | endpoint const& ep, | |
| 287 | reverse_flags flags, | |
| 288 | std::stop_token, | |
| 289 | std::error_code*, | |
| 290 | reverse_resolver_result*) override; | |
| 291 | ||
| 292 | void cancel() noexcept override; | |
| 293 | ||
| 294 | resolve_op op_; | |
| 295 | reverse_resolve_op reverse_op_; | |
| 296 | ||
| 297 | private: | |
| 298 | posix_resolver_service& svc_; | |
| 299 | }; | |
| 300 | ||
| 301 | } // namespace boost::corosio::detail | |
| 302 | ||
| 303 | #endif // BOOST_COROSIO_POSIX | |
| 304 | ||
| 305 | #endif // BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RESOLVER_HPP | |
| 306 |