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