Skip to main content

orbit_core/ring/
readiness.rs

1//! Native readiness bridge for an SHM ring.
2//!
3//! The shared signal is a generation in the ring header, waited through Linux
4//! futex or FreeBSD umtx. Each process owns a private `eventfd` and a small
5//! blocking driver thread that converts generation changes into fd readiness.
6//! Async runtimes can therefore wait on their normal reactor without sharing
7//! one drainable eventfd across readers.
8
9#![cfg(any(target_os = "linux", target_os = "freebsd"))]
10
11use std::fmt;
12use std::io;
13use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
16use std::thread::JoinHandle;
17
18use super::shm::ShmRing;
19
20/// Process-local fd readiness bridge for one shared Orbit ring.
21///
22/// Every subscribing process creates its own instance. Ring publishers bump a
23/// generation stored in SHM and wake all platform waiters; the local driver
24/// then marks this fd readable. Multiple publishes may coalesce into one wake,
25/// so a consumer must drain the fd and poll the ring through its own cursor.
26pub struct RingEventFd {
27    fd: OwnedFd,
28    ring: Arc<ShmRing>,
29    stop: Arc<AtomicBool>,
30    driver: Option<JoinHandle<()>>,
31}
32
33impl RingEventFd {
34    pub(crate) fn new(ring: Arc<ShmRing>) -> io::Result<Self> {
35        let raw_fd = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) };
36        if raw_fd < 0 {
37            return Err(io::Error::last_os_error());
38        }
39        let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
40        let driver_fd = fd.try_clone()?;
41        let stop = Arc::new(AtomicBool::new(false));
42        let driver_stop = stop.clone();
43        let driver_ring = ring.clone();
44        let mut observed = ring.notification_generation().load(Ordering::Acquire);
45
46        let driver = std::thread::Builder::new()
47            .name(format!("orbit-ring-{}-eventfd", ring.kind()))
48            .spawn(move || {
49                while !driver_stop.load(Ordering::Acquire) {
50                    let current = driver_ring
51                        .notification_generation()
52                        .load(Ordering::Acquire);
53                    if current != observed {
54                        observed = current;
55                        if signal_event_fd(driver_fd.as_raw_fd()).is_err() {
56                            break;
57                        }
58                        continue;
59                    }
60
61                    if wait_for_generation(driver_ring.notification_generation(), observed).is_err()
62                    {
63                        break;
64                    }
65                }
66            })?;
67
68        Ok(Self {
69            fd,
70            ring,
71            stop,
72            driver: Some(driver),
73        })
74    }
75
76    pub(crate) fn notify(ring: &ShmRing) -> io::Result<()> {
77        ring.notification_generation()
78            .fetch_add(1, Ordering::Release);
79        wake_all_generation_waiters(ring.notification_generation())
80    }
81
82    /// Drain all coalesced wake counts from this non-blocking eventfd.
83    ///
84    /// Ring events themselves remain in SHM; the returned number is only the
85    /// local wake count and must not be interpreted as an event count.
86    pub fn drain(&self) -> io::Result<u64> {
87        let mut total = 0u64;
88        loop {
89            let mut value = 0u64;
90            let read = unsafe {
91                libc::read(
92                    self.fd.as_raw_fd(),
93                    (&mut value as *mut u64).cast(),
94                    std::mem::size_of::<u64>(),
95                )
96            };
97            if read == std::mem::size_of::<u64>() as isize {
98                total = total.saturating_add(value);
99                continue;
100            }
101            if read == 0 {
102                return Err(io::Error::new(
103                    io::ErrorKind::UnexpectedEof,
104                    "Orbit eventfd closed while draining",
105                ));
106            }
107            if read < 0 {
108                let error = io::Error::last_os_error();
109                match error.raw_os_error() {
110                    Some(libc::EINTR) => continue,
111                    Some(libc::EAGAIN) => return Ok(total),
112                    _ => return Err(error),
113                }
114            }
115            return Err(io::Error::new(
116                io::ErrorKind::InvalidData,
117                "Orbit eventfd returned a partial counter",
118            ));
119        }
120    }
121}
122
123impl AsRawFd for RingEventFd {
124    fn as_raw_fd(&self) -> RawFd {
125        self.fd.as_raw_fd()
126    }
127}
128
129impl AsFd for RingEventFd {
130    fn as_fd(&self) -> BorrowedFd<'_> {
131        self.fd.as_fd()
132    }
133}
134
135impl fmt::Debug for RingEventFd {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.debug_struct("RingEventFd")
138            .field("fd", &self.fd.as_raw_fd())
139            .field("ring_kind", &self.ring.kind())
140            .finish_non_exhaustive()
141    }
142}
143
144impl Drop for RingEventFd {
145    fn drop(&mut self) {
146        self.stop.store(true, Ordering::Release);
147        // Change the generation before waking. If the driver passed its stop
148        // check but has not entered the platform wait yet, the atomic compare
149        // prevents it from parking after our wake and deadlocking join.
150        self.ring
151            .notification_generation()
152            .fetch_add(1, Ordering::Release);
153        let _ = wake_all_generation_waiters(self.ring.notification_generation());
154        if let Some(driver) = self.driver.take() {
155            let _ = driver.join();
156        }
157    }
158}
159
160#[cfg(target_os = "linux")]
161fn wait_for_generation(word: &AtomicU32, expected: u32) -> io::Result<()> {
162    let result = unsafe {
163        libc::syscall(
164            libc::SYS_futex,
165            word.as_ptr(),
166            libc::FUTEX_WAIT,
167            expected,
168            std::ptr::null::<libc::timespec>(),
169            std::ptr::null::<u32>(),
170            0,
171        )
172    };
173    if result == 0 {
174        return Ok(());
175    }
176
177    let error = io::Error::last_os_error();
178    match error.raw_os_error() {
179        // The generation changed before the kernel parked us, or the driver
180        // was interrupted. The outer loop re-checks both generation and stop.
181        Some(libc::EAGAIN) | Some(libc::EINTR) => Ok(()),
182        _ => Err(error),
183    }
184}
185
186#[cfg(target_os = "linux")]
187fn wake_all_generation_waiters(word: &AtomicU32) -> io::Result<()> {
188    let result = unsafe {
189        libc::syscall(
190            libc::SYS_futex,
191            word.as_ptr(),
192            libc::FUTEX_WAKE,
193            i32::MAX,
194            std::ptr::null::<libc::timespec>(),
195            std::ptr::null::<u32>(),
196            0,
197        )
198    };
199    if result >= 0 {
200        Ok(())
201    } else {
202        Err(io::Error::last_os_error())
203    }
204}
205
206#[cfg(target_os = "freebsd")]
207fn wait_for_generation(word: &AtomicU32, expected: u32) -> io::Result<()> {
208    let result = unsafe {
209        libc::_umtx_op(
210            word.as_ptr().cast(),
211            libc::UMTX_OP_WAIT_UINT,
212            expected as libc::c_ulong,
213            std::ptr::null_mut(),
214            std::ptr::null_mut(),
215        )
216    };
217    if result == 0 {
218        return Ok(());
219    }
220
221    let error = io::Error::last_os_error();
222    match error.raw_os_error() {
223        // The generation changed before the kernel parked us, or the driver
224        // was interrupted. The outer loop re-checks generation and stop.
225        Some(libc::EINTR) => Ok(()),
226        _ => Err(error),
227    }
228}
229
230#[cfg(target_os = "freebsd")]
231fn wake_all_generation_waiters(word: &AtomicU32) -> io::Result<()> {
232    let result = unsafe {
233        libc::_umtx_op(
234            word.as_ptr().cast(),
235            libc::UMTX_OP_WAKE,
236            i32::MAX as libc::c_ulong,
237            std::ptr::null_mut(),
238            std::ptr::null_mut(),
239        )
240    };
241    if result == 0 {
242        Ok(())
243    } else {
244        Err(io::Error::last_os_error())
245    }
246}
247
248fn signal_event_fd(fd: RawFd) -> io::Result<()> {
249    let value = 1u64;
250    loop {
251        let written = unsafe {
252            libc::write(
253                fd,
254                (&value as *const u64).cast(),
255                std::mem::size_of::<u64>(),
256            )
257        };
258        if written == std::mem::size_of::<u64>() as isize {
259            return Ok(());
260        }
261        if written < 0 {
262            let error = io::Error::last_os_error();
263            match error.raw_os_error() {
264                Some(libc::EINTR) => continue,
265                // A full eventfd is already readable, so the notification is
266                // represented even though this increment could not be added.
267                Some(libc::EAGAIN) => return Ok(()),
268                _ => return Err(error),
269            }
270        }
271        return Err(io::Error::new(
272            io::ErrorKind::WriteZero,
273            "Orbit eventfd accepted a partial counter",
274        ));
275    }
276}