Skip to main content

starnix_sync/
interruptible_event.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::sync::Arc;
6use std::sync::atomic::Ordering;
7
8/// A blocking object that can either be notified normally or interrupted
9///
10/// To block using an `InterruptibleEvent`, first call `begin_wait`. At this point, the event is
11/// in the "waiting" state, and future calls to `notify` or `interrupt` will terminate the wait.
12///
13/// After `begin_wait` returns, call `block_until` to block the current thread until one of the
14/// following conditions occur:
15///
16///  1. The given deadline expires.
17///  2. At least one of the `notify` or `interrupt` functions were called after `begin_wait`.
18///
19/// It's safe to call `notify` or `interrupt` at any time. However, calls to `begin_wait` and
20/// `block_until` must alternate, starting with `begin_wait`.
21///
22/// `InterruptibleEvent` uses two-phase waiting so that clients can register for notification,
23/// perform some related work, and then start blocking. This approach ensures that clients do not
24/// miss notifications that arrive after they perform the related work but before they actually
25/// start blocking.
26///
27/// # Priority Inheritance and Dynamic Futex Owner Assignment
28///
29/// `InterruptibleEvent` supports Zircon Priority Inheritance (PI). If the target owner thread is
30/// known at wait time, it can be passed directly to `block_until` / `zx_futex_wait`.
31///
32/// When the owner thread is not known when waiting begins (for example, when a transaction is
33/// queued to a process-wide worker pool), `assign_new_owner` can be called by the worker thread
34/// that later dequeues the work item. `assign_new_owner` uses `zx_futex_requeue` to atomically
35/// transfer waiting threads to a secondary `requeue_target` futex while designating the worker
36/// thread as the futex PI owner.
37///
38/// TODO(https://fxbug.dev/542307988): The long-term solution is a dedicated Zircon syscall (such as
39/// `zx_futex_assign_owner`) to assign or update the PI owner of an existing futex without
40/// requiring a secondary requeue target futex.
41#[derive(Debug)]
42pub struct InterruptibleEvent {
43    futex: zx::Futex,
44    requeue_target: zx::Futex,
45}
46
47/// The initial state.
48///
49///  * Transitions to `WAITING` after `begin_wait`.
50const READY: i32 = 0;
51
52/// The event is waiting for a notification or an interruption.
53///
54///  * Transitions to `NOTIFIED` after `notify`.
55///  * Transitions to `INTERRUPTED` after `interrupt`.
56///  * Transitions to `REQUEUED` after `assign_new_owner`.
57///  * Transitions to `READY` if the deadline for `block_until` expires.
58const WAITING: i32 = 1;
59
60/// The event has been notified and will wake up.
61///
62///  * Transitions to `READY` after `block_until` processes the notification.
63const NOTIFIED: i32 = 2;
64
65/// The event has been interrupted and will wake up.
66///
67///  * Transitions to `READY` after `block_until` processes the interruption.
68const INTERRUPTED: i32 = 3;
69
70/// The event has been requeued to a secondary futex target with a new PI owner.
71const REQUEUED: i32 = 4;
72
73/// A guard object to enforce that clients call `begin_wait` before `block_until`.
74#[must_use = "call block_until to advance the event state machine"]
75pub struct EventWaitGuard<'a> {
76    event: &'a Arc<InterruptibleEvent>,
77}
78
79impl<'a> EventWaitGuard<'a> {
80    /// The underlying event associated with this guard.
81    pub fn event(&self) -> &'a Arc<InterruptibleEvent> {
82        self.event
83    }
84
85    /// Returns the owner of the underlying futex, if any.
86    pub fn get_owner(&self) -> Option<zx::Koid> {
87        self.event.get_owner()
88    }
89
90    /// Block the thread until either `deadline` expires, the event is notified, or the event is
91    /// interrupted.
92    pub fn block_until(
93        self,
94        new_owner: Option<&zx::Thread>,
95        deadline: zx::MonotonicInstant,
96    ) -> Result<(), WakeReason> {
97        self.event.block_until(new_owner, deadline)
98    }
99}
100
101/// A description of why a `block_until` returned without the event being notified.
102#[derive(Debug, PartialEq, Eq)]
103pub enum WakeReason {
104    /// `block_until` returned because another thread interrupted the wait using `interrupt`.
105    Interrupted,
106
107    /// `block_until` returned because the given deadline expired.
108    DeadlineExpired,
109}
110
111impl Default for InterruptibleEvent {
112    fn default() -> Self {
113        InterruptibleEvent { futex: zx::Futex::new(READY), requeue_target: zx::Futex::new(READY) }
114    }
115}
116
117impl InterruptibleEvent {
118    pub fn new() -> Arc<Self> {
119        Arc::new(Self::default())
120    }
121
122    /// Returns the owner of the underlying futex or requeue target futex, if any.
123    pub fn get_owner(&self) -> Option<zx::Koid> {
124        self.futex.get_owner().or_else(|| self.requeue_target.get_owner())
125    }
126
127    /// Called to initiate a wait.
128    ///
129    /// Calls to `notify` or `interrupt` after this function returns will cause the event to wake
130    /// up. Calls to those functions prior to calling `begin_wait` will be ignored.
131    ///
132    /// Once called, this function cannot be called again until `block_until` returns. Otherwise,
133    /// this function will panic.
134    pub fn begin_wait<'a>(self: &'a Arc<Self>) -> EventWaitGuard<'a> {
135        self.requeue_target.store(READY, Ordering::Relaxed);
136        self.futex
137            .compare_exchange(READY, WAITING, Ordering::AcqRel, Ordering::Relaxed)
138            .expect("Tried to begin waiting on an event when not ready.");
139        EventWaitGuard { event: self }
140    }
141
142    /// Assigns `new_owner` as the Priority Inheritance (PI) owner for waiting thread(s).
143    ///
144    /// If threads are currently waiting on `futex`, this dynamically requeues them to
145    /// `requeue_target`, designating `new_owner` as the futex PI owner.
146    ///
147    /// This establishes Zircon Priority Inheritance (PI) from the waiting thread(s) to
148    /// `new_owner` when the owner was not known at `begin_wait` time.
149    ///
150    /// Note: This can only be called once per `begin_wait` cycle (while the event is in the
151    /// `WAITING` state). If the event is not waiting or has already been requeued/notified,
152    /// this returns `Err(zx::Status::BAD_STATE)`.
153    ///
154    /// TODO(https://fxbug.dev/542307988): Replace this requeue pattern with a dedicated Zircon
155    /// syscall to assign a new owner to a futex with waiting threads once available.
156    pub fn assign_new_owner(&self, new_owner: &zx::Thread) -> Result<(), zx::Status> {
157        if self
158            .futex
159            .compare_exchange(WAITING, REQUEUED, Ordering::AcqRel, Ordering::Relaxed)
160            .is_ok()
161        {
162            let res =
163                self.futex.requeue(0, REQUEUED, &self.requeue_target, u32::MAX, Some(new_owner));
164            // Setting `requeue_target` to `WAITING` after `zx_futex_requeue` ensures that any
165            // concurrent `wake()` loop does not issue `requeue_target.wake_all()` until after the
166            // kernel wait queue transfer has completed.
167            self.requeue_target.store(WAITING, Ordering::Release);
168            res
169        } else {
170            Err(zx::Status::BAD_STATE)
171        }
172    }
173
174    fn reset(&self) {
175        // We use a store here rather than a compare_exchange because other threads are
176        // only allowed to write to this value in the `WAITING` state and we are returning
177        // from a completed wait.
178        self.requeue_target.store(READY, Ordering::Relaxed);
179        self.futex.store(READY, Ordering::Relaxed);
180    }
181
182    fn block_until(
183        &self,
184        new_owner: Option<&zx::Thread>,
185        deadline: zx::MonotonicInstant,
186    ) -> Result<(), WakeReason> {
187        // We need to loop around the call to zx_futex_wait because we can receive spurious
188        // wakeups.
189        loop {
190            let futex_val = self.futex.load(Ordering::Acquire);
191            let is_requeued = futex_val == REQUEUED;
192            let target_futex = if is_requeued { &self.requeue_target } else { &self.futex };
193
194            match target_futex.wait(WAITING, new_owner, deadline) {
195                // The deadline expired while we were sleeping.
196                Err(zx::Status::TIMED_OUT) => {
197                    self.reset();
198                    return Err(WakeReason::DeadlineExpired);
199                }
200                // The value changed before we went to sleep. Note: If `assign_new_owner()` set
201                // `futex = REQUEUED` before `requeue_target` was initialized to `WAITING`,
202                // `target_futex.wait()` fails with `BAD_STATE`. In this case, `effective_state`
203                // evaluates to `WAITING`, causing the loop to retry on `requeue_target`.
204                Err(zx::Status::BAD_STATE) => (),
205                Err(e) => panic!("Unexpected error from zx_futex_wait: {e}"),
206                Ok(()) => (),
207            }
208
209            let state = target_futex.load(Ordering::Acquire);
210            let fallback_state = if is_requeued { futex_val } else { READY };
211
212            let effective_state = if state == NOTIFIED || state == INTERRUPTED {
213                state
214            } else if fallback_state == NOTIFIED || fallback_state == INTERRUPTED {
215                fallback_state
216            } else {
217                WAITING
218            };
219
220            let res = match effective_state {
221                // If we're still in the `WAITING` state, then the wake ended spuriously and we
222                // need to go back to sleep.
223                WAITING => continue,
224                NOTIFIED => Ok(()),
225                INTERRUPTED => Err(WakeReason::Interrupted),
226                _ => panic!("Unexpected event state: {effective_state}"),
227            };
228            self.reset();
229            return res;
230        }
231    }
232
233    /// Wake up the event normally.
234    ///
235    /// If this function is called before `begin_wait`, this notification is ignored. Calling this
236    /// function repeatedly has no effect. If both `notify` and `interrupt` are called, the state
237    /// observed by `block_until` is a race.
238    pub fn notify(&self) {
239        self.wake(NOTIFIED);
240    }
241
242    /// Wake up the event because of an interruption.
243    ///
244    /// If this function is called before `begin_wait`, this notification is ignored. Calling this
245    /// function repeatedly has no effect. If both `notify` and `interrupt` are called, the state
246    /// observed by `block_until` is a race.
247    pub fn interrupt(&self) {
248        self.wake(INTERRUPTED);
249    }
250
251    fn wake(&self, state: i32) {
252        // Fast path for standard (non-requeued) events:
253        // See <https://marabos.nl/atomics/hardware.html#failing-compare-exchange> for why we issue
254        // this relaxed load before the `compare_exchange` below. Checking `observed == WAITING`
255        // in Shared cache state prevents unnecessary exclusive cache line invalidations (RFOs)
256        // when the event is already NOTIFIED, INTERRUPTED, or REQUEUED.
257        //
258        // We specify `Ordering::Acquire` on failure so that if `compare_exchange` fails because
259        // another thread concurrently assigned a new owner (`futex` transitioned to `REQUEUED`),
260        // we observe the updated state with Acquire semantics before moving to the slow path.
261        let observed = self.futex.load(Ordering::Relaxed);
262        if observed == WAITING
263            && self
264                .futex
265                .compare_exchange(WAITING, state, Ordering::Release, Ordering::Acquire)
266                .is_ok()
267        {
268            self.futex.wake_all();
269            return;
270        }
271
272        // Slow path: `futex` was either already NOTIFIED/INTERRUPTED/READY, or `assign_new_owner()`
273        // claimed `futex` by setting it to `REQUEUED`.
274        //
275        // This loop only applies when `assign_new_owner()` is called (e.g. during Binder worker
276        // dispatch). When `assign_new_owner()` transitions `futex` to `REQUEUED`, there is a brief
277        // window before it initializes `requeue_target` to `WAITING` and executes
278        // `zx_futex_requeue`.
279        //
280        // We spin/yield until `requeue_target` transitions to `WAITING` (or until another thread
281        // notifies it), ensuring that we wake the waiter on `requeue_target` without missing
282        // notifications or deadlocking.
283        loop {
284            let futex_val = self.futex.load(Ordering::Acquire);
285            if futex_val == NOTIFIED || futex_val == INTERRUPTED || futex_val == READY {
286                return;
287            }
288
289            // futex_val is REQUEUED. Attempt to wake requeue_target once it is WAITING.
290            let requeue_val = self.requeue_target.load(Ordering::Acquire);
291            if requeue_val == WAITING {
292                if self
293                    .requeue_target
294                    .compare_exchange(WAITING, state, Ordering::Release, Ordering::Acquire)
295                    .is_ok()
296                {
297                    self.requeue_target.wake_all();
298                    return;
299                }
300            } else if requeue_val == NOTIFIED || requeue_val == INTERRUPTED {
301                return;
302            }
303
304            std::thread::yield_now();
305        }
306    }
307}
308
309#[cfg(test)]
310mod test {
311    use super::*;
312
313    #[test]
314    fn test_wait_block_and_notify() {
315        let event = InterruptibleEvent::new();
316
317        let guard = event.begin_wait();
318
319        let other_event = Arc::clone(&event);
320        let thread = std::thread::spawn(move || {
321            other_event.notify();
322        });
323
324        guard.block_until(None, zx::MonotonicInstant::INFINITE).expect("failed to be notified");
325        thread.join().expect("failed to join thread");
326    }
327
328    #[test]
329    fn test_wait_block_and_interrupt() {
330        let event = InterruptibleEvent::new();
331
332        let guard = event.begin_wait();
333
334        let other_event = Arc::clone(&event);
335        let thread = std::thread::spawn(move || {
336            other_event.interrupt();
337        });
338
339        let result = guard.block_until(None, zx::MonotonicInstant::INFINITE);
340        assert_eq!(result, Err(WakeReason::Interrupted));
341        thread.join().expect("failed to join thread");
342    }
343
344    #[test]
345    fn test_wait_block_and_timeout() {
346        let event = InterruptibleEvent::new();
347
348        let guard = event.begin_wait();
349        let result = guard
350            .block_until(None, zx::MonotonicInstant::after(zx::MonotonicDuration::from_millis(20)));
351        assert_eq!(result, Err(WakeReason::DeadlineExpired));
352    }
353
354    #[test]
355    fn futex_ownership_is_transferred() {
356        let event = Arc::new(InterruptibleEvent::new());
357
358        let (root_thread_handle, root_thread_koid) = fuchsia_runtime::with_thread_self(|thread| {
359            (thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(), thread.koid().unwrap())
360        });
361
362        let event_for_blocked_thread = event.clone();
363
364        let blocked_thread = std::thread::spawn(move || {
365            let event = event_for_blocked_thread;
366            let guard = event.begin_wait();
367            guard.block_until(Some(&root_thread_handle), zx::MonotonicInstant::INFINITE).unwrap();
368        });
369
370        // Wait for the correct owner to appear.
371        // TODO(b/502692311): Replace this polling loop if it starts timing out.
372        while event.get_owner() != Some(root_thread_koid) {
373            std::thread::sleep(std::time::Duration::from_millis(100));
374        }
375
376        event.notify();
377        blocked_thread.join().unwrap();
378    }
379
380    #[test]
381    fn stale_pi_owner_is_noop() {
382        let mut new_owner = None;
383        std::thread::scope(|s| {
384            s.spawn(|| {
385                new_owner = Some(
386                    fuchsia_runtime::with_thread_self(|thread| {
387                        thread.duplicate_handle(zx::Rights::SAME_RIGHTS)
388                    })
389                    .unwrap(),
390                );
391            });
392        });
393        let new_owner = new_owner.unwrap();
394
395        let event = InterruptibleEvent::new();
396        let guard = event.begin_wait();
397        let result = guard.block_until(
398            Some(&new_owner),
399            zx::MonotonicInstant::after(zx::MonotonicDuration::from_millis(20)),
400        );
401        assert_eq!(result, Err(WakeReason::DeadlineExpired));
402    }
403
404    #[test]
405    fn futex_ownership_is_transferred_via_requeue() {
406        let event = Arc::new(InterruptibleEvent::new());
407
408        let (root_thread_handle, root_thread_koid) = fuchsia_runtime::with_thread_self(|thread| {
409            (thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(), thread.koid().unwrap())
410        });
411
412        let event_for_blocked_thread = event.clone();
413
414        let (tx, rx) = std::sync::mpsc::channel();
415        let blocked_thread = std::thread::spawn(move || {
416            let (thread_handle, _) = fuchsia_runtime::with_thread_self(|thread| {
417                (thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(), thread.koid().unwrap())
418            });
419            tx.send(thread_handle).unwrap();
420            let event = event_for_blocked_thread;
421            let guard = event.begin_wait();
422            guard.block_until(None, zx::MonotonicInstant::INFINITE).unwrap();
423        });
424
425        // Wait until the thread starts sleeping on the primary futex in the kernel.
426        let blocked_thread_handle = rx.recv().unwrap();
427        while blocked_thread_handle.info().unwrap().state
428            != zx::ThreadState::Blocked(zx::ThreadBlockType::Futex)
429        {
430            std::thread::sleep(std::time::Duration::from_millis(10));
431        }
432
433        // Dynamically assign PI ownership to root_thread_handle.
434        event.assign_new_owner(&root_thread_handle).unwrap();
435
436        // Wait for the correct requeue owner to appear.
437        while event.get_owner() != Some(root_thread_koid) {
438            std::thread::sleep(std::time::Duration::from_millis(50));
439        }
440
441        event.notify();
442        blocked_thread.join().unwrap();
443    }
444
445    #[test]
446    fn concurrent_wait_wake_assign_new_owner_stress_test() {
447        let (root_thread_handle, _) = fuchsia_runtime::with_thread_self(|thread| {
448            (thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(), thread.koid().unwrap())
449        });
450
451        for _ in 0..200 {
452            let event = Arc::new(InterruptibleEvent::new());
453            let barrier = Arc::new(std::sync::Barrier::new(2));
454
455            let event_waiter = event.clone();
456            let event_assign = event.clone();
457            let event_waker = event.clone();
458            let barrier_assign = barrier.clone();
459            let barrier_waker = barrier.clone();
460            let thread_handle =
461                root_thread_handle.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
462
463            let waiter = std::thread::spawn(move || {
464                let guard = event_waiter.begin_wait();
465                let _ = guard.block_until(None, zx::MonotonicInstant::INFINITE);
466            });
467
468            while event.futex.load(Ordering::Relaxed) != WAITING {
469                std::thread::yield_now();
470            }
471
472            let assigner = std::thread::spawn(move || {
473                barrier_assign.wait();
474                let _ = event_assign.assign_new_owner(&thread_handle);
475            });
476
477            let waker = std::thread::spawn(move || {
478                barrier_waker.wait();
479                event_waker.notify();
480            });
481
482            assigner.join().unwrap();
483            waker.join().unwrap();
484            waiter.join().unwrap();
485        }
486    }
487}