nodedb_bridge/eventfd.rs
1// SPDX-License-Identifier: BUSL-1.1
2
3//! Cross-runtime wake signaling via Linux eventfd.
4//!
5//! eventfd is the only safe primitive for waking across Tokio and Glommio/monoio:
6//!
7//! - Both runtimes can poll a file descriptor.
8//! - No `Send` requirement on the waker itself — just read/write an fd.
9//! - Coalescing: multiple writes produce a single readable event.
10//!
11//! ## Usage
12//!
13//! Two EventFd instances per bridge channel:
14//!
15//! - `producer_wake`: Consumer writes → Producer reads (queue was full, now has space)
16//! - `consumer_wake`: Producer writes → Consumer reads (queue was empty, now has data)
17//!
18//! The runtime-specific integration (registering the fd with epoll/io_uring) is
19//! done by the caller. This module only provides raw fd-based signaling.
20
21use std::io;
22use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, RawFd};
23
24/// A cross-runtime wake signal backed by a Linux eventfd.
25///
26/// Write to signal, read to consume. Multiple signals coalesce into one.
27/// The fd can be registered with any event loop (epoll, io_uring, kqueue fallback).
28pub struct EventFd {
29 #[cfg(target_os = "linux")]
30 fd: OwnedFd,
31 #[cfg(not(target_os = "linux"))]
32 read_fd: OwnedFd,
33 #[cfg(not(target_os = "linux"))]
34 write_fd: OwnedFd,
35}
36
37impl EventFd {
38 /// Create a new eventfd in semaphore mode.
39 ///
40 /// `EFD_NONBLOCK` ensures reads/writes never block the calling thread.
41 /// `EFD_CLOEXEC` prevents fd leaks across fork/exec.
42 pub fn new() -> io::Result<Self> {
43 #[cfg(target_os = "linux")]
44 {
45 // SAFETY: eventfd2 is a standard Linux syscall. Flags are valid.
46 let fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
47 if fd < 0 {
48 return Err(io::Error::last_os_error());
49 }
50 // SAFETY: fd is a valid file descriptor returned by eventfd().
51 let fd = unsafe { OwnedFd::from_raw_fd(fd) };
52 Ok(Self { fd })
53 }
54 #[cfg(not(target_os = "linux"))]
55 {
56 let mut fds = [0; 2];
57 // SAFETY: `fds` points to two valid c_int slots for pipe to fill.
58 if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
59 return Err(io::Error::last_os_error());
60 }
61 if let Err(err) = set_pipe_flags(fds[0]).and_then(|()| set_pipe_flags(fds[1])) {
62 unsafe {
63 libc::close(fds[0]);
64 libc::close(fds[1]);
65 }
66 return Err(err);
67 }
68 // SAFETY: fds were returned by pipe() and ownership moves into OwnedFd.
69 let read_fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
70 let write_fd = unsafe { OwnedFd::from_raw_fd(fds[1]) };
71 Ok(Self { read_fd, write_fd })
72 }
73 }
74
75 /// Signal the other side to wake up.
76 ///
77 /// Writes 1 to the eventfd counter. Multiple writes accumulate but
78 /// a single read clears all pending signals.
79 pub fn notify(&self) -> io::Result<()> {
80 let val: u64 = 1;
81 #[cfg(target_os = "linux")]
82 let fd = self.fd.as_raw_fd();
83 #[cfg(not(target_os = "linux"))]
84 let fd = self.write_fd.as_raw_fd();
85 // SAFETY: writing 8 bytes to a valid eventfd.
86 let ret = unsafe { libc::write(fd, &val as *const u64 as *const libc::c_void, 8) };
87 if ret < 0 {
88 Err(io::Error::last_os_error())
89 } else {
90 Ok(())
91 }
92 }
93
94 /// Consume the pending signal count, returning the accumulated value.
95 ///
96 /// Returns `Ok(0)` if no signal was pending (EAGAIN on non-blocking fd).
97 /// Returns `Ok(n)` where n is the accumulated signal count.
98 pub fn try_read(&self) -> io::Result<u64> {
99 #[cfg(target_os = "linux")]
100 {
101 let mut val: u64 = 0;
102 // SAFETY: reading 8 bytes from a valid eventfd.
103 let ret = unsafe {
104 libc::read(
105 self.fd.as_raw_fd(),
106 &mut val as *mut u64 as *mut libc::c_void,
107 8,
108 )
109 };
110 if ret < 0 {
111 let err = io::Error::last_os_error();
112 if err.kind() == io::ErrorKind::WouldBlock {
113 Ok(0)
114 } else {
115 Err(err)
116 }
117 } else {
118 Ok(val)
119 }
120 }
121 #[cfg(not(target_os = "linux"))]
122 {
123 let mut count = 0u64;
124 loop {
125 let mut val: u64 = 0;
126 // SAFETY: reading 8 bytes from the nonblocking pipe read end.
127 let ret = unsafe {
128 libc::read(
129 self.read_fd.as_raw_fd(),
130 &mut val as *mut u64 as *mut libc::c_void,
131 8,
132 )
133 };
134 if ret == 8 {
135 count = count.saturating_add(val.max(1));
136 continue;
137 }
138 if ret < 0 {
139 let err = io::Error::last_os_error();
140 if err.kind() == io::ErrorKind::WouldBlock {
141 return Ok(count);
142 }
143 return Err(err);
144 }
145 return Ok(count);
146 }
147 }
148 }
149
150 /// Get the raw file descriptor for registration with an event loop.
151 ///
152 /// The caller can register this fd with:
153 /// - Tokio: `AsyncFd::new()`
154 /// - Glommio: `GlommioDma::from_raw_fd()` or similar
155 /// - io_uring: `IORING_OP_READ` on the fd
156 pub fn as_fd(&self) -> RawFd {
157 #[cfg(target_os = "linux")]
158 {
159 self.fd.as_raw_fd()
160 }
161 #[cfg(not(target_os = "linux"))]
162 {
163 self.read_fd.as_raw_fd()
164 }
165 }
166}
167
168#[cfg(not(target_os = "linux"))]
169fn set_pipe_flags(fd: RawFd) -> io::Result<()> {
170 // SAFETY: fcntl is called with a valid fd returned by pipe().
171 let status_flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
172 if status_flags < 0 {
173 return Err(io::Error::last_os_error());
174 }
175 // SAFETY: fcntl F_SETFL updates status flags for this fd.
176 if unsafe { libc::fcntl(fd, libc::F_SETFL, status_flags | libc::O_NONBLOCK) } < 0 {
177 return Err(io::Error::last_os_error());
178 }
179
180 // SAFETY: fcntl is called with a valid fd returned by pipe().
181 let fd_flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
182 if fd_flags < 0 {
183 return Err(io::Error::last_os_error());
184 }
185 // SAFETY: fcntl F_SETFD updates close-on-exec flags for this fd.
186 if unsafe { libc::fcntl(fd, libc::F_SETFD, fd_flags | libc::FD_CLOEXEC) } < 0 {
187 return Err(io::Error::last_os_error());
188 }
189 Ok(())
190}
191
192// SAFETY: eventfd is a kernel object. The fd can be shared across threads.
193// Writes are atomic (8-byte writes to eventfd are guaranteed atomic by Linux).
194unsafe impl Send for EventFd {}
195unsafe impl Sync for EventFd {}
196
197/// A pair of eventfds for bidirectional wake signaling across the bridge.
198///
199/// ```text
200/// Producer (Tokio) Consumer (TPC)
201/// │ │
202/// │── notify(consumer_wake) ──→│ "queue has data"
203/// │ │
204/// │←── notify(producer_wake) ──│ "queue has space"
205/// ```
206pub struct WakePair {
207 /// Producer reads this to know the consumer freed space.
208 pub producer_wake: EventFd,
209 /// Consumer reads this to know the producer enqueued data.
210 pub consumer_wake: EventFd,
211}
212
213impl WakePair {
214 /// Create a new wake pair.
215 pub fn new() -> io::Result<Self> {
216 Ok(Self {
217 producer_wake: EventFd::new()?,
218 consumer_wake: EventFd::new()?,
219 })
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn notify_and_read() {
229 let efd = EventFd::new().unwrap();
230
231 // No pending signal.
232 assert_eq!(efd.try_read().unwrap(), 0);
233
234 // Signal once.
235 efd.notify().unwrap();
236 assert_eq!(efd.try_read().unwrap(), 1);
237
238 // Consumed — nothing pending.
239 assert_eq!(efd.try_read().unwrap(), 0);
240 }
241
242 #[test]
243 fn multiple_notifies_accumulate() {
244 let efd = EventFd::new().unwrap();
245
246 efd.notify().unwrap();
247 efd.notify().unwrap();
248 efd.notify().unwrap();
249
250 // Single read returns accumulated count.
251 assert_eq!(efd.try_read().unwrap(), 3);
252 assert_eq!(efd.try_read().unwrap(), 0);
253 }
254
255 #[test]
256 fn wake_pair_bidirectional() {
257 let pair = WakePair::new().unwrap();
258
259 // Producer signals consumer.
260 pair.consumer_wake.notify().unwrap();
261 assert_eq!(pair.consumer_wake.try_read().unwrap(), 1);
262
263 // Consumer signals producer.
264 pair.producer_wake.notify().unwrap();
265 assert_eq!(pair.producer_wake.try_read().unwrap(), 1);
266 }
267
268 #[test]
269 fn fd_is_valid() {
270 let efd = EventFd::new().unwrap();
271 assert!(efd.as_fd() >= 0);
272 }
273}