FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
reactor.hpp
Go to the documentation of this file.
1#pragma once
2// Single-threaded event loop with a timer min-heap and a thread-safe mailbox (post/wake via
3// eventfd), over one of two backends chosen at construction:
4//
5// ReactorBackend::Epoll edge-triggered epoll (the default)
6// ReactorBackend::IoUring io_uring, one multishot IORING_OP_POLL_ADD per registered fd,
7// timeouts through IORING_ENTER_EXT_ARG, over liburing
8//
9// All handler callbacks and timers run on the thread that calls run()/run_once(); only wake()
10// and post() may be called from other threads.
11//
12// Handler contract (both backends): a handler receiving on_readable()/on_writable() must drain
13// the fd until it returns want_read/want_write, otherwise no further event may be delivered. The
14// io_uring backend may report readiness the handler has already consumed; draining until EAGAIN
15// makes that harmless. POLLOUT is only requested while the registration asks for Write.
16//
17// Descriptor lifetime: call remove() before closing a registered fd. epoll forgets a closed
18// descriptor on its own, but an io_uring poll request holds a reference to the file, so the
19// socket stays open (no FIN, port still bound) until remove() cancels the request. add() of a
20// new socket that reuses the number of a closed but still registered fd cancels the stale
21// registration; this cannot be detected for anonymous-inode descriptors (eventfd, timerfd).
22#include "fastmm/net/inplace_callback.hpp"
23
24#include <atomic>
25#include <cstddef>
26#include <cstdint>
27#include <functional>
28#include <memory>
29#include <mutex>
30#include <string_view>
31#include <vector>
32
33struct io_uring_sqe; // NOLINT(readability-identifier-naming): <liburing.h>
34
35namespace fastmm::net {
36
37namespace detail {
38class IoUring;
39}
40
41// NOLINTNEXTLINE(performance-enum-size): same width as the epoll event mask it is converted to
42enum class IoEvent : std::uint32_t { None = 0, Read = 1, Write = 2, ReadWrite = 3 };
44constexpr IoEvent operator|(IoEvent a, IoEvent b) noexcept {
45 return static_cast<IoEvent>(static_cast<std::uint32_t>(a) | static_cast<std::uint32_t>(b));
46}
47constexpr bool has(IoEvent set, IoEvent flag) noexcept {
48 return (static_cast<std::uint32_t>(set) & static_cast<std::uint32_t>(flag)) != 0;
49}
50
51enum class ReactorBackend : std::uint8_t { Epoll, IoUring };
53// "epoll" / "io_uring", the spelling of [engine] net_backend.
54[[nodiscard]] std::string_view to_string(ReactorBackend backend) noexcept;
55// Parses "epoll" / "io_uring". Returns false (and leaves `out` alone) for anything else.
56[[nodiscard]] bool parse_reactor_backend(std::string_view text, ReactorBackend& out) noexcept;
58class IoHandler {
59 public:
60 virtual ~IoHandler() = default;
61 virtual void on_readable() = 0;
62 virtual void on_writable() = 0;
63 // POLLERR (or a poll request the kernel refused). `err` is the socket's SO_ERROR when
64 // retrievable (the refused request's errno for the latter), else 0.
65 virtual void on_error(int err) = 0;
66};
67
68using TimerId = std::uint64_t;
69inline constexpr TimerId kInvalidTimer = 0;
71class Reactor {
72 public:
73 // Stored inline (no heap): captures up to InplaceCallback::kCapacity bytes.
74 using TimerCallback = InplaceCallback;
75 using Task = std::function<void()>;
78 // Throws std::runtime_error if the backend (epoll_create1 / io_uring_setup) or the eventfd
79 // cannot be created. Check io_uring_supported() before asking for IoUring.
82 Reactor(const Reactor&) = delete;
83 Reactor& operator=(const Reactor&) = delete;
85 [[nodiscard]] ReactorBackend backend() const noexcept { return backend_; }
86 // True when this kernel lets the process create an io_uring with everything the backend uses
87 // (multishot poll, poll update, EXT_ARG timeouts). Probed once, then cached. False on ENOSYS,
88 // EPERM (kernel.io_uring_disabled, seccomp), ENOMEM and kernels older than 5.13.
89 [[nodiscard]] static bool io_uring_supported() noexcept;
90 // `requested`, or Epoll when io_uring was requested but is not supported.
91 [[nodiscard]] static ReactorBackend resolve_backend(ReactorBackend requested) noexcept;
93 // fd registration (reactor thread only). Registrations always include RDHUP so half-closes
94 // surface as on_readable() (read returns EOF). add() fails for an fd that is already
95 // registered, a closed fd and (like epoll) regular files and directories.
96 bool add(int fd, IoHandler& handler, IoEvent events) noexcept;
97 bool modify(int fd, IoHandler& handler, IoEvent events) noexcept;
98 bool remove(int fd) noexcept;
99 bool is_registered(int fd) const noexcept;
101 // Timers (reactor thread only). Deadlines are on the now_ns() clock. Arming a timer allocates
102 // only when more timers are live than ever before (slot and heap growth).
103 TimerId add_timer(std::int64_t deadline_ns, TimerCallback cb);
104 TimerId add_timer_after(std::int64_t delay_ns, TimerCallback cb) {
105 return add_timer(now_ns() + delay_ns, std::move(cb));
106 }
107 bool cancel_timer(TimerId id) noexcept;
108 std::size_t active_timers() const noexcept { return active_timers_; }
110 // Thread-safe: queue a task for the reactor thread and wake it.
111 void post(Task task);
112 // Thread-safe: interrupt a blocking wait in run_once().
113 void wake() noexcept;
114 // The eventfd wake() writes. Another process that holds a duplicate (the gateway passes it to
115 // the strategy with SCM_RIGHTS) wakes this reactor by writing 1 to it.
116 [[nodiscard]] int wake_fd() const noexcept { return wake_fd_; }
118 // One iteration: wait for I/O at most min(next timer, max_wait_ms), dispatch I/O, posted
119 // tasks, expired timers. max_wait_ms < 0 blocks until something happens. Returns the number
120 // of I/O events (epoll) or completions (io_uring) dispatched, or -1 on a wait error other than
121 // EINTR. epoll rounds the timer bound up to whole milliseconds; io_uring waits in nanoseconds.
122 int run_once(int max_wait_ms);
123 // Loop until `stop` becomes true (checked once per iteration; call wake() after setting it).
124 void run(const std::atomic<bool>& stop);
126 // Busy polling: never block, pause instruction between empty polls. epoll calls epoll_wait
127 // with a zero timeout; io_uring reads the completion ring without a system call unless the
128 // kernel flags pending work.
129 void set_busy_poll(bool enabled) noexcept { busy_poll_ = enabled; }
130 bool busy_poll() const noexcept { return busy_poll_; }
132 // Called on the reactor thread after the handler of each I/O event, between one socket's event
133 // and the next of the same wait: work that must not wait for the whole batch to be handled
134 // (the engine's orders behind a burst of market data, live/venue_slot.cpp) goes here. Null: none.
135 using EventHook = void (*)(void* ctx);
136 void set_after_event(EventHook fn, void* ctx) noexcept {
137 after_event_ = fn;
138 after_event_ctx_ = ctx;
139 }
140
141 // True while a handler runs for an event that also reported a hang-up or an error
142 // (RDHUP/HUP/ERR). Only when it is false may a handler take a short read as "socket drained"
143 // and skip the read that would return EAGAIN: edge-triggered readiness reports data that
144 // arrives later with a new event, but an end of stream already reported has no second event.
145 bool event_hangup() const noexcept { return event_hangup_; }
147 static std::int64_t now_ns() noexcept; // CLOCK_MONOTONIC
148 // The epoll descriptor, or -1 with the io_uring backend.
149 int epoll_fd() const noexcept { return epoll_fd_; }
151 private:
152 struct TimerEntry {
153 std::int64_t deadline_ns;
154 TimerId id;
155 bool operator>(const TimerEntry& o) const noexcept {
156 return deadline_ns != o.deadline_ns ? deadline_ns > o.deadline_ns : id > o.id;
157 }
158 };
159 // io_uring registration state, indexed by fd.
160 struct FdState {
161 std::uint32_t gen = 0; // bumped by add(); stale completions carry an older value
162 std::uint32_t mask = 0; // poll events currently requested
163 std::uint64_t dev = 0; // identity of the registered file, to detect fd number reuse
164 std::uint64_t ino = 0;
165 };
166
167 void release() noexcept;
168 int compute_timeout_ms(int max_wait_ms) const noexcept;
169 std::int64_t compute_timeout_ns(int max_wait_ms) const noexcept;
170 void dispatch_io(int nfds);
171 void deliver(int fd, IoHandler* h, bool error, bool readable, bool writable);
172 void run_posted();
173 void run_expired_timers();
174 void drain_wake_fd() noexcept;
175 static int socket_error(int fd) noexcept;
176
177 // io_uring backend
178 int uring_run_once(int max_wait_ms);
179 int uring_dispatch();
180 void uring_complete(std::uint64_t user_data, std::int32_t res, std::uint32_t flags);
181 bool uring_add(int fd, IoHandler& handler, IoEvent events) noexcept;
182 io_uring_sqe* uring_sqe() noexcept;
183 bool uring_arm(int fd) noexcept;
184 bool uring_queue_update(int fd, std::uint32_t mask) noexcept;
185 bool uring_queue_remove(int fd, std::uint32_t gen) noexcept;
186
187 ReactorBackend backend_;
188 int epoll_fd_ = -1;
189 int wake_fd_ = -1;
190 bool busy_poll_ = false;
191 bool event_hangup_ = false;
192 EventHook after_event_ = nullptr;
193 void* after_event_ctx_ = nullptr;
194 std::vector<IoHandler*> handlers_; // indexed by fd; nullptr when not registered
195
196 // Live timers by slot. A TimerId is (sequence << kTimerSlotBits) | slot, so the id of a cancelled
197 // or fired timer no longer matches its slot once the slot is reused.
198 static constexpr unsigned kTimerSlotBits = 24;
199 static constexpr TimerId kTimerSlotMask = (TimerId{1} << kTimerSlotBits) - 1;
200 struct TimerSlot {
202 TimerCallback cb;
203 };
204 [[nodiscard]] bool timer_live(TimerId id) const noexcept {
205 return timer_slots_[id & kTimerSlotMask].id == id;
206 }
207 void release_timer_slot(std::size_t slot) noexcept;
208
209 std::vector<TimerEntry> heap_; // min-heap on deadline; cancelled entries are skipped
210 std::vector<TimerSlot> timer_slots_;
211 std::vector<std::uint32_t> free_timer_slots_;
212 std::size_t active_timers_ = 0;
213 TimerId next_timer_seq_ = 1;
214
215 std::mutex post_mutex_;
216 std::atomic<bool> posted_pending_{false}; // posted_ is not empty; lets run_posted skip the lock
217 std::vector<Task> posted_;
218 std::vector<Task> running_; // swapped in under the lock, executed unlocked
219
220 static constexpr int kMaxEvents = 64;
221 void* events_ = nullptr; // epoll_event[kMaxEvents]; kept opaque to avoid <sys/epoll.h> here
222
223 std::unique_ptr<detail::IoUring> uring_;
224 std::vector<FdState> fd_state_;
225};
226
227} // namespace fastmm::net
Definition reactor.hpp:59
virtual void on_error(int err)=0
POLLERR (or a poll request the kernel refused).
virtual ~IoHandler()=default
virtual void on_readable()=0
virtual void on_writable()=0
Definition reactor.hpp:72
void wake() noexcept
Thread-safe: interrupt a blocking wait in run_once().
bool event_hangup() const noexcept
True while a handler runs for an event that also reported a hang-up or an error (RDHUP/HUP/ERR).
Definition reactor.hpp:146
void run(const std::atomic< bool > &stop)
Loop until stop becomes true (checked once per iteration; call wake() after setting it).
std::size_t active_timers() const noexcept
Definition reactor.hpp:109
bool is_registered(int fd) const noexcept
TimerId add_timer_after(std::int64_t delay_ns, TimerCallback cb)
Definition reactor.hpp:105
bool cancel_timer(TimerId id) noexcept
InplaceCallback TimerCallback
Stored inline (no heap): captures up to InplaceCallback::kCapacity bytes.
Definition reactor.hpp:75
bool add(int fd, IoHandler &handler, IoEvent events) noexcept
fd registration (reactor thread only).
Reactor()
Definition reactor.hpp:78
Reactor & operator=(const Reactor &)=delete
TimerId add_timer(std::int64_t deadline_ns, TimerCallback cb)
Timers (reactor thread only).
static bool io_uring_supported() noexcept
True when this kernel lets the process create an io_uring with everything the backend uses (multishot...
int epoll_fd() const noexcept
The epoll descriptor, or -1 with the io_uring backend.
Definition reactor.hpp:150
int run_once(int max_wait_ms)
One iteration: wait for I/O at most min(next timer, max_wait_ms), dispatch I/O, posted tasks,...
void set_after_event(EventHook fn, void *ctx) noexcept
Definition reactor.hpp:137
void set_busy_poll(bool enabled) noexcept
Busy polling: never block, pause instruction between empty polls.
Definition reactor.hpp:130
int wake_fd() const noexcept
The eventfd wake() writes.
Definition reactor.hpp:117
static ReactorBackend resolve_backend(ReactorBackend requested) noexcept
requested, or Epoll when io_uring was requested but is not supported.
bool modify(int fd, IoHandler &handler, IoEvent events) noexcept
std::function< void()> Task
Definition reactor.hpp:76
void post(Task task)
Thread-safe: queue a task for the reactor thread and wake it.
static std::int64_t now_ns() noexcept
CLOCK_MONOTONIC.
void(*)(void *ctx) EventHook
Called on the reactor thread after the handler of each I/O event, between one socket's event and the ...
Definition reactor.hpp:136
ReactorBackend backend() const noexcept
Definition reactor.hpp:86
bool remove(int fd) noexcept
bool busy_poll() const noexcept
Definition reactor.hpp:131
Definition reactor.hpp:37
constexpr IoEvent operator|(IoEvent a, IoEvent b) noexcept
Definition reactor.hpp:45
std::uint64_t TimerId
Definition reactor.hpp:69
std::string_view to_string(ReactorBackend backend) noexcept
"epoll" / "io_uring", the spelling of [engine] net_backend.
ReactorBackend
Definition reactor.hpp:52
constexpr bool has(IoEvent set, IoEvent flag) noexcept
Definition reactor.hpp:48
bool parse_reactor_backend(std::string_view text, ReactorBackend &out) noexcept
Parses "epoll" / "io_uring". Returns false (and leaves out alone) for anything else.
IoEvent
Definition reactor.hpp:43
constexpr TimerId kInvalidTimer
Definition reactor.hpp:70