Skip to content

Commit 68217eb

Browse files
committed
fix(runtime): add self-pipe EventFd fallback for non-Linux core wake
The data-plane core wake had no functional backend outside Linux: EventFd::new() returned an Unsupported error on non-Linux targets, so spawn_core() failed and the server aborted during data-plane bootstrap. Add a self-pipe fallback for non-Linux Unix targets (macOS, BSD), mirroring the pipe-based EventFd already used in nodedb-bridge: notify() writes a byte, drain() reads until empty, poll_wait() polls the read end, and both ends are non-blocking + close-on-exec. Linux continues to use eventfd in semaphore mode; non-Unix targets (wasm, Windows) still return an explicit error. This lets the server boot and serve requests on macOS while keeping the eventfd fast path unchanged on Linux.
1 parent 4036df0 commit 68217eb

1 file changed

Lines changed: 153 additions & 20 deletions

File tree

nodedb/src/data/eventfd.rs

Lines changed: 153 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,39 @@
11
// SPDX-License-Identifier: BUSL-1.1
22

3-
//! Thin wrapper around Linux `eventfd` for TPC core wake signaling.
3+
//! Cross-thread core wake signaling for the Data Plane.
44
//!
55
//! When the Control Plane pushes a request into the SPSC ring buffer, it
6-
//! writes to the eventfd to wake the Data Plane core from `libc::poll`.
7-
//! This replaces the 50µs busy-poll sleep with an interrupt-driven wake.
6+
//! signals the Data Plane core to wake from `libc::poll`. This replaces the
7+
//! 50µs busy-poll sleep with an interrupt-driven wake.
8+
//!
9+
//! ## Platform backends
10+
//! - **Linux**: `eventfd` in `EFD_SEMAPHORE` mode — the production path.
11+
//! - **Other Unix (macOS, BSD)**: a self-pipe — slower per-wake than eventfd
12+
//! but functionally identical, so the server boots and serves on developer
13+
//! machines. This is a dev/correctness path, not a performance path.
14+
//! - **Non-Unix (wasm, Windows)**: unsupported; `EventFd::new()` errors.
815
9-
#[cfg(not(target_arch = "wasm32"))]
16+
#[cfg(unix)]
1017
use std::os::unix::io::RawFd;
11-
#[cfg(target_arch = "wasm32")]
18+
#[cfg(not(unix))]
1219
type RawFd = i32;
1320

14-
/// An eventfd file descriptor for cross-thread wake signaling.
21+
/// A file descriptor for cross-thread wake signaling.
1522
///
1623
/// `!Send` and `!Sync` — each core owns its own EventFd on the Data Plane side.
1724
/// The Control Plane holds a cloneable `EventFdNotifier` (which is `Send + Sync`).
1825
pub struct EventFd {
1926
#[cfg(target_os = "linux")]
2027
fd: RawFd,
28+
// Self-pipe: read end lives with the core, the notifier writes to `write_fd`.
29+
#[cfg(all(unix, not(target_os = "linux")))]
30+
read_fd: RawFd,
31+
#[cfg(all(unix, not(target_os = "linux")))]
32+
write_fd: RawFd,
2133
}
2234

2335
impl EventFd {
24-
/// Create a new eventfd in semaphore mode (EFD_SEMAPHORE).
36+
/// Create a new core-wake handle.
2537
pub fn new() -> crate::Result<Self> {
2638
#[cfg(target_os = "linux")]
2739
{
@@ -33,11 +45,33 @@ impl EventFd {
3345
}
3446
Ok(Self { fd })
3547
}
36-
#[cfg(not(target_os = "linux"))]
48+
#[cfg(all(unix, not(target_os = "linux")))]
49+
{
50+
let mut fds = [0 as libc::c_int; 2];
51+
// SAFETY: `fds` points to two valid c_int slots for pipe() to fill.
52+
if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
53+
return Err(crate::Error::Io(std::io::Error::last_os_error()));
54+
}
55+
// Both ends non-blocking + close-on-exec: drain() must not block on
56+
// an empty pipe, and notify() must not block on a full one.
57+
if let Err(e) = set_pipe_flags(fds[0]).and_then(|()| set_pipe_flags(fds[1])) {
58+
// SAFETY: both fds were just returned by pipe() and are still open.
59+
unsafe {
60+
libc::close(fds[0]);
61+
libc::close(fds[1]);
62+
}
63+
return Err(crate::Error::Io(e));
64+
}
65+
Ok(Self {
66+
read_fd: fds[0],
67+
write_fd: fds[1],
68+
})
69+
}
70+
#[cfg(not(unix))]
3771
{
3872
Err(crate::Error::Io(std::io::Error::new(
3973
std::io::ErrorKind::Unsupported,
40-
"eventfd-backed data-plane wake is only available on Linux",
74+
"data-plane core wake is only available on Unix platforms",
4175
)))
4276
}
4377
}
@@ -48,7 +82,11 @@ impl EventFd {
4882
{
4983
self.fd
5084
}
51-
#[cfg(not(target_os = "linux"))]
85+
#[cfg(all(unix, not(target_os = "linux")))]
86+
{
87+
self.read_fd
88+
}
89+
#[cfg(not(unix))]
5290
{
5391
-1
5492
}
@@ -72,7 +110,34 @@ impl EventFd {
72110
};
73111
if ret == 8 { buf } else { 0 }
74112
}
75-
#[cfg(not(target_os = "linux"))]
113+
#[cfg(all(unix, not(target_os = "linux")))]
114+
{
115+
// Read every queued wake byte until the pipe drains (EAGAIN). The
116+
// caller's `while drain() > 0 {}` loop relies on a 0 once empty.
117+
let mut total: u64 = 0;
118+
let mut buf = [0u8; 256];
119+
loop {
120+
// SAFETY: reading into a valid local buffer from a non-blocking fd.
121+
let ret = unsafe {
122+
libc::read(
123+
self.read_fd,
124+
buf.as_mut_ptr() as *mut libc::c_void,
125+
buf.len(),
126+
)
127+
};
128+
if ret > 0 {
129+
total = total.saturating_add(ret as u64);
130+
// A short read means the pipe is now empty.
131+
if (ret as usize) < buf.len() {
132+
return total;
133+
}
134+
continue;
135+
}
136+
// ret == 0 (EOF) or ret < 0 (EAGAIN / error): nothing more to read.
137+
return total;
138+
}
139+
}
140+
#[cfg(not(unix))]
76141
{
77142
0
78143
}
@@ -82,18 +147,18 @@ impl EventFd {
82147
///
83148
/// Returns `true` if a signal was received, `false` on timeout.
84149
pub fn poll_wait(&self, timeout_ms: i32) -> bool {
85-
#[cfg(target_os = "linux")]
150+
#[cfg(unix)]
86151
{
87152
let mut pfd = libc::pollfd {
88-
fd: self.fd,
153+
fd: self.as_raw_fd(),
89154
events: libc::POLLIN,
90155
revents: 0,
91156
};
92157
// SAFETY: standard poll syscall on a valid fd.
93158
let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
94159
ret > 0 && (pfd.revents & libc::POLLIN) != 0
95160
}
96-
#[cfg(not(target_os = "linux"))]
161+
#[cfg(not(unix))]
97162
{
98163
let _ = timeout_ms;
99164
false
@@ -106,7 +171,11 @@ impl EventFd {
106171
{
107172
EventFdNotifier { fd: self.fd }
108173
}
109-
#[cfg(not(target_os = "linux"))]
174+
#[cfg(all(unix, not(target_os = "linux")))]
175+
{
176+
EventFdNotifier { fd: self.write_fd }
177+
}
178+
#[cfg(not(unix))]
110179
{
111180
EventFdNotifier {}
112181
}
@@ -116,23 +185,60 @@ impl EventFd {
116185
#[cfg(target_os = "linux")]
117186
impl Drop for EventFd {
118187
fn drop(&mut self) {
188+
// SAFETY: `self.fd` is owned by this EventFd and closed exactly once.
119189
unsafe {
120190
libc::close(self.fd);
121191
}
122192
}
123193
}
124194

125-
/// A `Send + Sync` handle for signaling an eventfd from the Control Plane.
195+
#[cfg(all(unix, not(target_os = "linux")))]
196+
impl Drop for EventFd {
197+
fn drop(&mut self) {
198+
// SAFETY: both pipe ends are owned by this EventFd and closed once.
199+
unsafe {
200+
libc::close(self.read_fd);
201+
libc::close(self.write_fd);
202+
}
203+
}
204+
}
205+
206+
/// Set `O_NONBLOCK` and `FD_CLOEXEC` on a freshly-created pipe fd.
207+
#[cfg(all(unix, not(target_os = "linux")))]
208+
fn set_pipe_flags(fd: RawFd) -> std::io::Result<()> {
209+
// SAFETY: fcntl is called with a valid fd returned by pipe().
210+
let status_flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
211+
if status_flags < 0 {
212+
return Err(std::io::Error::last_os_error());
213+
}
214+
// SAFETY: F_SETFL updates the fd's status flags.
215+
if unsafe { libc::fcntl(fd, libc::F_SETFL, status_flags | libc::O_NONBLOCK) } < 0 {
216+
return Err(std::io::Error::last_os_error());
217+
}
218+
// SAFETY: fcntl is called with a valid fd returned by pipe().
219+
let fd_flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
220+
if fd_flags < 0 {
221+
return Err(std::io::Error::last_os_error());
222+
}
223+
// SAFETY: F_SETFD updates the fd's close-on-exec flag.
224+
if unsafe { libc::fcntl(fd, libc::F_SETFD, fd_flags | libc::FD_CLOEXEC) } < 0 {
225+
return Err(std::io::Error::last_os_error());
226+
}
227+
Ok(())
228+
}
229+
230+
/// A `Send + Sync` handle for signaling an `EventFd` from the Control Plane.
126231
///
127232
/// The underlying fd is owned by the `EventFd` on the Data Plane side.
128233
/// The notifier only writes to it — it does not close the fd on drop.
129234
#[derive(Clone, Copy)]
130235
pub struct EventFdNotifier {
131-
#[cfg(target_os = "linux")]
236+
#[cfg(unix)]
132237
fd: RawFd,
133238
}
134239

135-
// SAFETY: eventfd write is thread-safe and atomic for 8-byte writes.
240+
// SAFETY: the underlying write (eventfd 8-byte / pipe 1-byte) is atomic and
241+
// thread-safe, so the notifier can be shared and copied across threads.
136242
unsafe impl Send for EventFdNotifier {}
137243
unsafe impl Sync for EventFdNotifier {}
138244

@@ -152,10 +258,20 @@ impl EventFdNotifier {
152258
);
153259
}
154260
}
261+
#[cfg(all(unix, not(target_os = "linux")))]
262+
{
263+
let val: u8 = 1;
264+
// SAFETY: writing 1 byte to the self-pipe write end. A full pipe
265+
// (EAGAIN) is intentionally ignored — it already means a wake is
266+
// pending, so the level-triggered poll will still fire.
267+
unsafe {
268+
libc::write(self.fd, &val as *const u8 as *const libc::c_void, 1);
269+
}
270+
}
155271
}
156272
}
157273

158-
#[cfg(all(test, target_os = "linux"))]
274+
#[cfg(all(test, unix))]
159275
mod tests {
160276
use super::*;
161277

@@ -167,7 +283,8 @@ mod tests {
167283
// No signals yet.
168284
assert_eq!(efd.drain(), 0);
169285

170-
// Signal and drain.
286+
// Signal and drain — one notify yields one pending signal on both
287+
// backends (eventfd semaphore read == 1, self-pipe byte == 1).
171288
notifier.notify();
172289
assert_eq!(efd.drain(), 1);
173290

@@ -176,6 +293,7 @@ mod tests {
176293
}
177294

178295
#[test]
296+
#[cfg(target_os = "linux")]
179297
fn multiple_signals_accumulate() {
180298
let efd = EventFd::new().unwrap();
181299
let notifier = efd.notifier();
@@ -191,6 +309,21 @@ mod tests {
191309
assert_eq!(efd.drain(), 0);
192310
}
193311

312+
#[test]
313+
#[cfg(all(unix, not(target_os = "linux")))]
314+
fn multiple_signals_coalesce() {
315+
let efd = EventFd::new().unwrap();
316+
let notifier = efd.notifier();
317+
318+
notifier.notify();
319+
notifier.notify();
320+
notifier.notify();
321+
322+
// Self-pipe: a single drain consumes all queued wake bytes, then 0.
323+
assert_eq!(efd.drain(), 3);
324+
assert_eq!(efd.drain(), 0);
325+
}
326+
194327
#[test]
195328
fn poll_wait_timeout() {
196329
let efd = EventFd::new().unwrap();

0 commit comments

Comments
 (0)