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 ) ]
1017use std:: os:: unix:: io:: RawFd ;
11- #[ cfg( target_arch = "wasm32" ) ]
18+ #[ cfg( not ( unix ) ) ]
1219type 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`).
1825pub 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
2335impl 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" ) ]
117186impl 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 ) ]
130235pub 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.
136242unsafe impl Send for EventFdNotifier { }
137243unsafe 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 ) ) ]
159275mod 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