Skip to main content

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}