include/boost/corosio/native/detail/reactor/reactor_descriptor_state.hpp

90.4% Lines (94 / 104) 100.0% Functions (5 / 5)
reactor_descriptor_state.hpp
f(x) Functions (5)
Line TLA Hits 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_REACTOR_REACTOR_DESCRIPTOR_STATE_HPP
12 #define BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_DESCRIPTOR_STATE_HPP
13
14 #include <boost/corosio/native/detail/reactor/reactor_events.hpp>
15 #include <boost/corosio/native/detail/reactor/reactor_op_base.hpp>
16 #include <boost/corosio/native/detail/reactor/reactor_scheduler.hpp>
17 #include <boost/corosio/detail/ready_queue.hpp>
18
19 #include <boost/corosio/detail/conditionally_enabled_mutex.hpp>
20
21 #include <atomic>
22 #include <cstdint>
23 #include <memory>
24
25 #include <errno.h>
26 #include <sys/socket.h>
27
28 namespace boost::corosio::detail {
29
30 /** Per-descriptor state shared across reactor backends.
31
32 Tracks pending operations for a file descriptor. The fd is registered
33 once with the reactor and stays registered until closed. Uses deferred
34 I/O: the reactor sets ready_events atomically, then enqueues this state.
35 When popped by the scheduler, invoke_deferred_io() performs I/O under
36 the mutex and queues completed ops.
37
38 Non-template: uses reactor_op_base pointers so the scheduler and
39 descriptor_state code exist as a single copy in the binary regardless
40 of how many backends are compiled in.
41
42 @par Thread Safety
43 The mutex protects operation pointers and ready flags. ready_events_
44 and is_enqueued_ are atomic for lock-free reactor access.
45 */
46 struct reactor_descriptor_state
47 : scheduler_op
48 , reactor_retirable
49 {
50 /// Protects operation pointers and ready/cancel flags.
51 /// Becomes a no-op in single-threaded mode.
52 conditionally_enabled_mutex mutex{true};
53
54 /// Pending read operation (guarded by `mutex`).
55 reactor_op_base* read_op = nullptr;
56
57 /// Pending write operation (guarded by `mutex`).
58 reactor_op_base* write_op = nullptr;
59
60 /// Pending connect operation (guarded by `mutex`).
61 reactor_op_base* connect_op = nullptr;
62
63 /// Pending wait-for-read operation (guarded by `mutex`).
64 reactor_op_base* wait_read_op = nullptr;
65
66 /// Pending wait-for-write operation (guarded by `mutex`).
67 reactor_op_base* wait_write_op = nullptr;
68
69 /// Pending wait-for-error operation (guarded by `mutex`).
70 reactor_op_base* wait_error_op = nullptr;
71
72 /// True if a read edge event arrived before an op was registered.
73 bool read_ready = false;
74
75 /// True if a write edge event arrived before an op was registered.
76 bool write_ready = false;
77
78 /// Event mask set during registration (no mutex needed).
79 std::uint32_t registered_events = 0;
80
81 /// The reactor refused to watch this fd (e.g. /dev/null on epoll);
82 /// its I/O never blocks, and an op that would park must not.
83 bool unpollable = false;
84
85 /// File descriptor this state tracks.
86 int fd = -1;
87
88 /// Accumulated ready events (set by reactor, read by scheduler).
89 std::atomic<std::uint32_t> ready_events_{0};
90
91 /// True while this state is queued in the scheduler's completed_ops.
92 std::atomic<bool> is_enqueued_{false};
93
94 /// Owning scheduler for posting completions.
95 reactor_scheduler const* scheduler_ = nullptr;
96
97 /// Prevents impl destruction while queued in the scheduler.
98 detail::object_ref object_ref_;
99
100 /// Add ready events atomically.
101 /// Release pairs with the consumer's acquire exchange on
102 /// ready_events_ so the consumer sees all flags. Acquire pairs with
103 /// registration's release store, so a reactor pass on another
104 /// thread sees the state registration initialized before handing
105 /// it to the kernel. On x86 (TSO) this compiles to the same LOCK OR
106 /// as relaxed.
107 124497x void add_ready_events(std::uint32_t ev) noexcept
108 {
109 124497x ready_events_.fetch_or(ev, std::memory_order_acq_rel);
110 124497x }
111
112 /// Invoke deferred I/O and dispatch completions.
113 54214x void operator()() override
114 {
115 54214x invoke_deferred_io();
116 54214x }
117
118 /// Move @p keep into this state if it is queued without one.
119 22992x void hand_off(object_ref& keep) noexcept override
120 {
121 // Locked even to read the flag: a popped invocation clears it at
122 // the start of its critical section and still uses this state
123 // until it unlocks, so an unlocked false does not mean idle.
124 22992x conditionally_enabled_mutex::scoped_lock lock(mutex);
125 22992x if (is_enqueued_.load(std::memory_order_acquire) && !object_ref_)
126 886x object_ref_ = std::move(keep);
127 22992x }
128
129 /// Destroy without invoking.
130 /// Called during scheduler::shutdown() drain. Clear object_ref_ to break
131 /// the self-referential cycle set by close_socket().
132 228x void destroy() override
133 {
134 228x object_ref_.reset();
135 228x }
136
137 /** Perform deferred I/O and queue completions.
138
139 Performs I/O under the mutex and queues completed ops. EAGAIN
140 ops stay parked in their slot for re-delivery on the next
141 edge event.
142 */
143 void invoke_deferred_io();
144 };
145
146 inline void
147 54216x reactor_descriptor_state::invoke_deferred_io()
148 {
149 54216x detail::object_ref prevent_impl_destruction;
150 54216x ready_queue local_ops;
151 // Read under the lock: once it drops, a retired owner may be
152 // recycled, so nothing below touches this state.
153 54216x reactor_scheduler const* sched = nullptr;
154
155 {
156 54216x conditionally_enabled_mutex::scoped_lock lock(mutex);
157 54216x sched = scheduler_;
158
159 // Must clear is_enqueued_ and move object_ref_ under the same
160 // lock that processes I/O. close_socket() checks is_enqueued_
161 // under this mutex — without atomicity between the flag store
162 // and the ref move, close_socket() could see is_enqueued_==false,
163 // skip setting object_ref_, and destroy the impl under us.
164 54216x prevent_impl_destruction = std::move(object_ref_);
165 54216x is_enqueued_.store(false, std::memory_order_release);
166
167 54216x std::uint32_t ev = ready_events_.exchange(0, std::memory_order_acquire);
168 54216x if (ev == 0)
169 {
170 // Mutex unlocks here; compensate for work_cleanup's decrement
171 ✗ scheduler_->compensating_work_started();
172 ✗ return;
173 }
174
175 54216x int err = 0;
176 54216x if (ev & reactor_event_error)
177 {
178 // Force the read/write dispatch below to run: an
179 // edge-triggered EPOLLERR can arrive alone, and without this
180 // a parked op never calls perform_io() and, the edge being
181 // one-shot, never gets another chance -- a permanent hang.
182 // Every parked op then re-runs its own syscall or probe.
183 //
184 // Assumes at least one parked op's own syscall makes
185 // non-EAGAIN progress; if every op re-parks with EAGAIN this
186 // sticky error is never redelivered and they hang. No such
187 // case is known -- a future descriptor type that hits one
188 // should be handled here.
189 365x ev |= reactor_event_read | reactor_event_write;
190
191 // SO_ERROR clears on read, so take it only for a parked op
192 // that reports it. A readiness wait reports readiness and
193 // leaves the error for the next read or write to name, as
194 // asio does; reading it here with nothing to report it to
195 // would turn a reset into a clean EOF.
196 365x bool const reports_error =
197 365x read_op || write_op || connect_op || wait_error_op;
198 365x socklen_t len = sizeof(err);
199 395x if (reports_error &&
200 30x ::getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) < 0)
201 {
202 // Non-socket fd (pipe, chardev, ...): no SO_ERROR, so
203 // let the op's own syscall name the real failure.
204 11x err = (errno == ENOTSOCK) ? 0 : errno;
205 }
206 // select raises its exceptional set for out-of-band/urgent
207 // data as well as for genuine faults; on a healthy socket the
208 // probe then reads SO_ERROR == 0. Faulting a pending read or
209 // write on that is wrong, so an I/O operation completes only
210 // on a real (non-zero) error. wait(error) still names a code
211 // below.
212 }
213
214 54216x if (ev & reactor_event_read)
215 {
216 22332x if (read_op)
217 {
218 7702x auto* rd = read_op;
219 7702x if (err)
220 6x rd->complete(err, 0);
221 else
222 7696x rd->perform_io();
223
224 7702x if (rd->errn == EAGAIN || rd->errn == EWOULDBLOCK)
225 {
226 5x rd->errn = 0;
227 }
228 else
229 {
230 7697x read_op = nullptr;
231 7697x local_ops.push(rd);
232 }
233 }
234 else
235 {
236 14630x read_ready = true;
237 }
238
239 // The event does not prove the socket is still readable: a
240 // parked read op above may have drained it, or a speculative
241 // read consumed the data before this dispatch ran. The wait
242 // op's perform_io() re-probes and reports EAGAIN to stay
243 // parked.
244 22332x if (wait_read_op)
245 {
246 38x auto* wo = wait_read_op;
247 38x wo->perform_io();
248
249 38x if (wo->errn == EAGAIN || wo->errn == EWOULDBLOCK)
250 {
251 2x wo->errn = 0;
252 }
253 else
254 {
255 36x wait_read_op = nullptr;
256 36x local_ops.push(wo);
257 }
258 }
259 }
260 54216x if (ev & reactor_event_write)
261 {
262 46690x bool had_write_op = (connect_op || write_op);
263 // A writable event on a socket still in SYN_SENT (e.g. the
264 // spurious pre-connect readiness of a fresh socket) must
265 // not complete the connect; perform_io() reports EAGAIN
266 // until a peer is actually established.
267 46690x if (connect_op)
268 {
269 8110x auto* cn = connect_op;
270 8110x if (err)
271 12x cn->complete(err, 0);
272 else
273 8098x cn->perform_io();
274
275 8110x if (cn->errn == EAGAIN || cn->errn == EWOULDBLOCK)
276 {
277 ✗ cn->errn = 0;
278 }
279 else
280 {
281 8110x connect_op = nullptr;
282 8110x local_ops.push(cn);
283 }
284 }
285 46690x if (write_op)
286 {
287 200x auto* wr = write_op;
288 200x if (err)
289 2x wr->complete(err, 0);
290 else
291 198x wr->perform_io();
292
293 200x if (wr->errn == EAGAIN || wr->errn == EWOULDBLOCK)
294 {
295 1x wr->errn = 0;
296 }
297 else
298 {
299 199x write_op = nullptr;
300 199x local_ops.push(wr);
301 }
302 }
303 46690x if (!had_write_op)
304 38380x write_ready = true;
305
306 // Same re-probe discipline as the wait-for-read dispatch.
307 46690x if (wait_write_op)
308 {
309 11x auto* wo = wait_write_op;
310 11x wo->perform_io();
311
312 11x if (wo->errn == EAGAIN || wo->errn == EWOULDBLOCK)
313 {
314 ✗ wo->errn = 0;
315 }
316 else
317 {
318 11x wait_write_op = nullptr;
319 11x local_ops.push(wo);
320 }
321 }
322 }
323 // Complete a parked wait-for-error on any error condition.
324 54216x if (ev & reactor_event_error)
325 {
326 365x if (wait_error_op)
327 {
328 // wait(error) fired on the exceptional condition; name a
329 // code even when the kernel exposed none (e.g. urgent
330 // data leaves SO_ERROR == 0).
331 6x int const werr = err ? err : EIO;
332 6x wait_error_op->complete(werr, 0);
333 6x local_ops.push(std::exchange(wait_error_op, nullptr));
334 }
335 }
336 54216x if (err)
337 {
338 23x if (read_op)
339 {
340 ✗ read_op->complete(err, 0);
341 ✗ local_ops.push(std::exchange(read_op, nullptr));
342 }
343 23x if (write_op)
344 {
345 ✗ write_op->complete(err, 0);
346 ✗ local_ops.push(std::exchange(write_op, nullptr));
347 }
348 23x if (connect_op)
349 {
350 ✗ connect_op->complete(err, 0);
351 ✗ local_ops.push(std::exchange(connect_op, nullptr));
352 }
353 }
354 54216x }
355
356 // Execute first handler inline — the scheduler's work_cleanup
357 // accounts for this as the "consumed" work item. local_ops holds
358 // only ops, so the popped entry decodes directly.
359 54216x scheduler_op* first = ready_as_op(local_ops.pop());
360 54216x if (first)
361 {
362 16057x sched->post_deferred_completions(local_ops);
363 16057x (*first)();
364 }
365 else
366 {
367 38159x sched->compensating_work_started();
368 }
369 54216x }
370
371 } // namespace boost::corosio::detail
372
373 #endif // BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_DESCRIPTOR_STATE_HPP
374