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    /// Retains the PI owner assigned via `assign_new_owner` for the duration of the wait cycle.
46    ///
47    /// Keeping an `Arc<zx::Thread>` reference here (rather than only passing the handle once to
48    /// `zx_futex_requeue`) is required for three reasons:
49    ///  1. When `block_until` wakes at the intermediate `clean_deadline` to call `clean_stack()`,
50    ///     Zircon's `FutexWait` (`ResetOwnerIfNoWaiters`) clears `requeue_target`'s kernel owner
51    ///     before returning `ZX_ERR_TIMED_OUT`; re-waiting on `requeue_target` must pass the owner
52    ///     handle again to preserve PI for the rest of the wait.
53    ///  2. If `assign_new_owner` is called after `begin_wait` but before the waiting thread enters
54    ///     `zx_futex_wait` in the kernel (or if a spurious wakeup occurs), `zx_futex_requeue` moves
55    ///     0 kernel waiters, and a subsequent `requeue_target.wait(WAITING, None, ...)` would
56    ///     overwrite `requeue_target`'s kernel owner with `None`.
57    ///  3. Holding an `Arc<zx::Thread>` guarantees the underlying `zx_handle_t` stays open until
58    ///     `reset()`, avoiding `zx_handle_duplicate` syscalls and `ZX_POL_BAD_HANDLE` policy
59    ///     exceptions if the worker thread exits during the wait.
60    requeue_owner: fuchsia_sync::Mutex<Option<Arc<zx::Thread>>>,
61}
62
63/// The initial state.
64///
65///  * Transitions to `WAITING` after `begin_wait`.
66const READY: i32 = 0;
67
68/// The event is waiting for a notification or an interruption.
69///
70///  * Transitions to `NOTIFIED` after `notify`.
71///  * Transitions to `INTERRUPTED` after `interrupt`.
72///  * Transitions to `REQUEUED` after `assign_new_owner`.
73///  * Transitions to `READY` if the deadline for `block_until` expires.
74const WAITING: i32 = 1;
75
76/// The event has been notified and will wake up.
77///
78///  * Transitions to `READY` after `block_until` processes the notification.
79const NOTIFIED: i32 = 2;
80
81/// The event has been interrupted and will wake up.
82///
83///  * Transitions to `READY` after `block_until` processes the interruption.
84const INTERRUPTED: i32 = 3;
85
86/// The event has been requeued to a secondary futex target with a new PI owner.
87const REQUEUED: i32 = 4;
88
89/// A guard object to enforce that clients call `begin_wait` before `block_until`.
90#[must_use = "call block_until to advance the event state machine"]
91pub struct EventWaitGuard<'a> {
92    event: &'a Arc<InterruptibleEvent>,
93}
94
95impl<'a> EventWaitGuard<'a> {
96    /// The underlying event associated with this guard.
97    pub fn event(&self) -> &'a Arc<InterruptibleEvent> {
98        self.event
99    }
100
101    /// Returns the owner of the underlying futex, if any.
102    pub fn get_owner(&self) -> Option<zx::Koid> {
103        self.event.get_owner()
104    }
105
106    /// Block the thread until either `deadline` expires, the event is notified, or the event is
107    /// interrupted.
108    pub fn block_until(
109        self,
110        new_owner: Option<&zx::Thread>,
111        deadline: zx::MonotonicInstant,
112    ) -> Result<(), WakeReason> {
113        self.event.block_until_with_clean_delay(new_owner, deadline, crate::STACK_CLEAN_DELAY)
114    }
115
116    #[cfg(test)]
117    fn block_until_with_clean_delay(
118        self,
119        new_owner: Option<&zx::Thread>,
120        deadline: zx::MonotonicInstant,
121        clean_delay: zx::MonotonicDuration,
122    ) -> Result<(), WakeReason> {
123        self.event.block_until_with_clean_delay(new_owner, deadline, clean_delay)
124    }
125}
126
127/// A description of why a `block_until` returned without the event being notified.
128#[derive(Debug, PartialEq, Eq)]
129pub enum WakeReason {
130    /// `block_until` returned because another thread interrupted the wait using `interrupt`.
131    Interrupted,
132
133    /// `block_until` returned because the given deadline expired.
134    DeadlineExpired,
135}
136
137impl Default for InterruptibleEvent {
138    fn default() -> Self {
139        InterruptibleEvent {
140            futex: zx::Futex::new(READY),
141            requeue_target: zx::Futex::new(READY),
142            requeue_owner: fuchsia_sync::Mutex::new(None),
143        }
144    }
145}
146
147impl InterruptibleEvent {
148    pub fn new() -> Arc<Self> {
149        Arc::new(Self::default())
150    }
151
152    /// Returns the owner of the underlying futex or requeue target futex, if any.
153    pub fn get_owner(&self) -> Option<zx::Koid> {
154        self.futex.get_owner().or_else(|| self.requeue_target.get_owner())
155    }
156
157    /// Called to initiate a wait.
158    ///
159    /// Calls to `notify` or `interrupt` after this function returns will cause the event to wake
160    /// up. Calls to those functions prior to calling `begin_wait` will be ignored.
161    ///
162    /// Once called, this function cannot be called again until `block_until` returns. Otherwise,
163    /// this function will panic.
164    pub fn begin_wait<'a>(self: &'a Arc<Self>) -> EventWaitGuard<'a> {
165        self.requeue_target.store(READY, Ordering::Relaxed);
166        self.futex
167            .compare_exchange(READY, WAITING, Ordering::AcqRel, Ordering::Relaxed)
168            .expect("Tried to begin waiting on an event when not ready.");
169        EventWaitGuard { event: self }
170    }
171
172    /// Assigns `new_owner` as the Priority Inheritance (PI) owner for waiting thread(s).
173    ///
174    /// If threads are currently waiting on `futex`, this dynamically requeues them to
175    /// `requeue_target`, designating `new_owner` as the futex PI owner.
176    ///
177    /// This establishes Zircon Priority Inheritance (PI) from the waiting thread(s) to
178    /// `new_owner` when the owner was not known at `begin_wait` time.
179    ///
180    /// Note: This can only be called once per `begin_wait` cycle (while the event is in the
181    /// `WAITING` state). If the event is not waiting or has already been requeued/notified,
182    /// this returns `Err(zx::Status::BAD_STATE)`.
183    ///
184    /// TODO(https://fxbug.dev/542307988): Replace this requeue pattern with a dedicated Zircon
185    /// syscall to assign a new owner to a futex with waiting threads once available.
186    pub fn assign_new_owner(&self, new_owner: &Arc<zx::Thread>) -> Result<(), zx::Status> {
187        if self.futex.load(Ordering::Relaxed) != WAITING {
188            return Err(zx::Status::BAD_STATE);
189        }
190        // Hold `requeue_owner` across the `WAITING -> REQUEUED` transition, `zx_futex_requeue`,
191        // and `requeue_target` initialization so that any thread in `block_until` or `reset` that
192        // observes `futex == REQUEUED` and locks `requeue_owner` is serialized after the requeue
193        // completes (with kernel Priority Inheritance from `fuchsia_sync::Mutex` if
194        // `assign_new_owner` was preempted).
195        let mut requeue_owner = self.requeue_owner.lock();
196        if self
197            .futex
198            .compare_exchange(WAITING, REQUEUED, Ordering::AcqRel, Ordering::Relaxed)
199            .is_ok()
200        {
201            *requeue_owner = Some(Arc::clone(new_owner));
202            let res =
203                self.futex.requeue(0, REQUEUED, &self.requeue_target, u32::MAX, Some(new_owner));
204            // Setting `requeue_target` to `WAITING` after `zx_futex_requeue` ensures that any
205            // concurrent `wake()` loop does not issue `requeue_target.wake_all()` until after the
206            // kernel wait queue transfer has completed.
207            self.requeue_target.store(WAITING, Ordering::Release);
208            res
209        } else {
210            Err(zx::Status::BAD_STATE)
211        }
212    }
213
214    fn reset(&self) {
215        // `reset()` is only called at the end of `block_until`, which consumes the unique
216        // `EventWaitGuard` created by `begin_wait` (when `futex == READY`). Because `futex` is not
217        // set back to `READY` until the end of this function, at most one thread can execute
218        // `reset()` at a time.
219        //
220        // On normal non-requeued wakeups where `futex` is already `NOTIFIED` or `INTERRUPTED`,
221        // `assign_new_owner` cannot transition `futex` to `REQUEUED` and `requeue_target` is
222        // already `READY`, so we only need to serialize against `assign_new_owner` when `futex` is
223        // `WAITING` or `REQUEUED`.
224        let observed = self.futex.load(Ordering::Relaxed);
225        if observed == WAITING || observed == REQUEUED {
226            if self.futex.swap(NOTIFIED, Ordering::AcqRel) == REQUEUED {
227                *self.requeue_owner.lock() = None;
228            }
229            self.requeue_target.store(READY, Ordering::Relaxed);
230        }
231        // Use `Ordering::Release` so clearing `requeue_owner` and resetting `requeue_target`
232        // happen-before a subsequent `begin_wait()`'s `AcqRel` CAS from `READY` to `WAITING`.
233        self.futex.store(READY, Ordering::Release);
234    }
235
236    fn block_until_with_clean_delay(
237        &self,
238        new_owner: Option<&zx::Thread>,
239        deadline: zx::MonotonicInstant,
240        clean_delay: zx::MonotonicDuration,
241    ) -> Result<(), WakeReason> {
242        // We need to loop around the call to zx_futex_wait because we can receive spurious
243        // wakeups. Clean the stack once if the wait remains blocked for `clean_delay`.
244        let mut clean_deadline = crate::initial_clean_deadline(deadline, clean_delay);
245        loop {
246            let futex_val = self.futex.load(Ordering::Acquire);
247            let is_requeued = futex_val == REQUEUED;
248            let requeued_owner = if is_requeued { self.requeue_owner.lock().clone() } else { None };
249            let (target_futex, effective_owner) = if is_requeued {
250                (&self.requeue_target, requeued_owner.as_deref())
251            } else {
252                (&self.futex, new_owner)
253            };
254
255            match target_futex.wait(WAITING, effective_owner, clean_deadline) {
256                // The deadline expired while we were sleeping.
257                Err(zx::Status::TIMED_OUT) => {
258                    if clean_deadline < deadline {
259                        starnix_stack::clean_stack();
260                        clean_deadline = deadline;
261                        // Continue immediately so we re-read `self.futex` and `requeue_owner` in
262                        // case `assign_new_owner` requeued us during the initial `clean_delay`
263                        // sleep.
264                        continue;
265                    } else {
266                        self.reset();
267                        return Err(WakeReason::DeadlineExpired);
268                    }
269                }
270                // The value changed before we went to sleep. Note: If `assign_new_owner()` set
271                // `futex = REQUEUED` before `requeue_target` was initialized to `WAITING`,
272                // `target_futex.wait()` fails with `BAD_STATE`. In this case, `state` evaluates to
273                // `WAITING` or `READY`, causing the loop to retry on `requeue_target`.
274                Err(zx::Status::BAD_STATE) => (),
275                Err(e) => panic!("Unexpected error from zx_futex_wait: {e}"),
276                Ok(()) => (),
277            }
278
279            let mut state = target_futex.load(Ordering::Acquire);
280            if state == REQUEUED {
281                state = self.requeue_target.load(Ordering::Acquire);
282            }
283
284            let res = match state {
285                // If we're still in the `WAITING` state (or `READY` while `assign_new_owner` is
286                // initializing `requeue_target`), then the wake ended spuriously and we need to go
287                // back to sleep.
288                WAITING | READY => continue,
289                NOTIFIED => Ok(()),
290                INTERRUPTED => Err(WakeReason::Interrupted),
291                _ => panic!("Unexpected event state: {state}"),
292            };
293            self.reset();
294            return res;
295        }
296    }
297
298    /// Wake up the event normally.
299    ///
300    /// If this function is called before `begin_wait`, this notification is ignored. Calling this
301    /// function repeatedly has no effect. If both `notify` and `interrupt` are called, the state
302    /// observed by `block_until` is a race.
303    pub fn notify(&self) {
304        self.wake(NOTIFIED);
305    }
306
307    /// Wake up the event because of an interruption.
308    ///
309    /// If this function is called before `begin_wait`, this notification is ignored. Calling this
310    /// function repeatedly has no effect. If both `notify` and `interrupt` are called, the state
311    /// observed by `block_until` is a race.
312    pub fn interrupt(&self) {
313        self.wake(INTERRUPTED);
314    }
315
316    fn wake(&self, state: i32) {
317        // Fast path for standard (non-requeued) events:
318        // See <https://marabos.nl/atomics/hardware.html#failing-compare-exchange> for why we issue
319        // this relaxed load before the `compare_exchange` below. Checking `observed == WAITING`
320        // in Shared cache state prevents unnecessary exclusive cache line invalidations (RFOs)
321        // when the event is already NOTIFIED, INTERRUPTED, or REQUEUED.
322        //
323        // We specify `Ordering::Acquire` on failure so that if `compare_exchange` fails because
324        // another thread concurrently assigned a new owner (`futex` transitioned to `REQUEUED`),
325        // we observe the updated state with Acquire semantics before moving to the slow path.
326        let observed = self.futex.load(Ordering::Relaxed);
327        if observed == WAITING
328            && self
329                .futex
330                .compare_exchange(WAITING, state, Ordering::Release, Ordering::Acquire)
331                .is_ok()
332        {
333            self.futex.wake_all();
334            return;
335        }
336
337        // Slow path: `futex` was either already NOTIFIED/INTERRUPTED/READY, or `assign_new_owner()`
338        // claimed `futex` by setting it to `REQUEUED`.
339        //
340        // This loop only applies when `assign_new_owner()` is called (e.g. during Binder worker
341        // dispatch). When `assign_new_owner()` transitions `futex` to `REQUEUED`, there is a brief
342        // window before it initializes `requeue_target` to `WAITING` and executes
343        // `zx_futex_requeue`.
344        //
345        // We spin/yield until `requeue_target` transitions to `WAITING` (or until another thread
346        // notifies it), ensuring that we wake the waiter on `requeue_target` without missing
347        // notifications or deadlocking.
348        loop {
349            let futex_val = self.futex.load(Ordering::Acquire);
350            if futex_val == NOTIFIED || futex_val == INTERRUPTED || futex_val == READY {
351                return;
352            }
353
354            // futex_val is REQUEUED. Attempt to wake requeue_target once it is WAITING.
355            let requeue_val = self.requeue_target.load(Ordering::Acquire);
356            if requeue_val == WAITING {
357                if self
358                    .requeue_target
359                    .compare_exchange(WAITING, state, Ordering::Release, Ordering::Acquire)
360                    .is_ok()
361                {
362                    self.requeue_target.wake_all();
363                    return;
364                }
365            } else if requeue_val == NOTIFIED || requeue_val == INTERRUPTED {
366                return;
367            }
368
369            std::thread::yield_now();
370        }
371    }
372}
373
374#[cfg(test)]
375mod test {
376    use super::*;
377
378    #[test]
379    fn test_wait_block_and_notify() {
380        let event = InterruptibleEvent::new();
381
382        let guard = event.begin_wait();
383
384        let other_event = Arc::clone(&event);
385        let thread = std::thread::spawn(move || {
386            other_event.notify();
387        });
388
389        guard.block_until(None, zx::MonotonicInstant::INFINITE).expect("failed to be notified");
390        thread.join().expect("failed to join thread");
391    }
392
393    #[test]
394    fn test_wait_block_and_interrupt() {
395        let event = InterruptibleEvent::new();
396
397        let guard = event.begin_wait();
398
399        let other_event = Arc::clone(&event);
400        let thread = std::thread::spawn(move || {
401            other_event.interrupt();
402        });
403
404        let result = guard.block_until(None, zx::MonotonicInstant::INFINITE);
405        assert_eq!(result, Err(WakeReason::Interrupted));
406        thread.join().expect("failed to join thread");
407    }
408
409    #[test]
410    fn test_wait_block_and_timeout() {
411        let event = InterruptibleEvent::new();
412
413        let guard = event.begin_wait();
414        let result = guard
415            .block_until(None, zx::MonotonicInstant::after(zx::MonotonicDuration::from_millis(20)));
416        assert_eq!(result, Err(WakeReason::DeadlineExpired));
417    }
418
419    #[test]
420    fn futex_ownership_is_transferred() {
421        let event = Arc::new(InterruptibleEvent::new());
422
423        let (root_thread_handle, root_thread_koid) = fuchsia_runtime::with_thread_self(|thread| {
424            (thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(), thread.koid().unwrap())
425        });
426
427        let event_for_blocked_thread = event.clone();
428
429        let blocked_thread = std::thread::spawn(move || {
430            let event = event_for_blocked_thread;
431            let guard = event.begin_wait();
432            guard.block_until(Some(&root_thread_handle), zx::MonotonicInstant::INFINITE).unwrap();
433        });
434
435        // Wait for the correct owner to appear.
436        // TODO(b/502692311): Replace this polling loop if it starts timing out.
437        while event.get_owner() != Some(root_thread_koid) {
438            std::thread::sleep(std::time::Duration::from_millis(100));
439        }
440
441        event.notify();
442        blocked_thread.join().unwrap();
443    }
444
445    #[test]
446    fn stale_pi_owner_is_noop() {
447        let mut new_owner = None;
448        std::thread::scope(|s| {
449            s.spawn(|| {
450                new_owner = Some(
451                    fuchsia_runtime::with_thread_self(|thread| {
452                        thread.duplicate_handle(zx::Rights::SAME_RIGHTS)
453                    })
454                    .unwrap(),
455                );
456            });
457        });
458        let new_owner = new_owner.unwrap();
459
460        let event = InterruptibleEvent::new();
461        let guard = event.begin_wait();
462        let result = guard.block_until(
463            Some(&new_owner),
464            zx::MonotonicInstant::after(zx::MonotonicDuration::from_millis(20)),
465        );
466        assert_eq!(result, Err(WakeReason::DeadlineExpired));
467    }
468
469    #[test]
470    fn futex_ownership_is_transferred_via_requeue() {
471        let event = Arc::new(InterruptibleEvent::new());
472
473        let (root_thread_handle, root_thread_koid) = fuchsia_runtime::with_thread_self(|thread| {
474            (
475                Arc::new(thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap()),
476                thread.koid().unwrap(),
477            )
478        });
479
480        let event_for_blocked_thread = event.clone();
481
482        let (tx, rx) = std::sync::mpsc::channel();
483        let blocked_thread = std::thread::spawn(move || {
484            let (thread_handle, _) = fuchsia_runtime::with_thread_self(|thread| {
485                (thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap(), thread.koid().unwrap())
486            });
487            tx.send(thread_handle).unwrap();
488            let event = event_for_blocked_thread;
489            let guard = event.begin_wait();
490            guard.block_until(None, zx::MonotonicInstant::INFINITE).unwrap();
491        });
492
493        // Wait until the thread starts sleeping on the primary futex in the kernel.
494        let blocked_thread_handle = rx.recv().unwrap();
495        while blocked_thread_handle.info().unwrap().state
496            != zx::ThreadState::Blocked(zx::ThreadBlockType::Futex)
497        {
498            std::thread::sleep(std::time::Duration::from_millis(10));
499        }
500
501        // Dynamically assign PI ownership to root_thread_handle.
502        event.assign_new_owner(&root_thread_handle).unwrap();
503
504        // Wait for the correct requeue owner to appear.
505        while event.get_owner() != Some(root_thread_koid) {
506            std::thread::sleep(std::time::Duration::from_millis(50));
507        }
508
509        event.notify();
510        blocked_thread.join().unwrap();
511    }
512
513    #[test]
514    fn concurrent_wait_wake_assign_new_owner_stress_test() {
515        let root_thread_handle = fuchsia_runtime::with_thread_self(|thread| {
516            Arc::new(thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap())
517        });
518
519        for _ in 0..200 {
520            let event = Arc::new(InterruptibleEvent::new());
521            let barrier = Arc::new(std::sync::Barrier::new(2));
522
523            let event_waiter = event.clone();
524            let event_assign = event.clone();
525            let event_waker = event.clone();
526            let barrier_assign = barrier.clone();
527            let barrier_waker = barrier.clone();
528            let thread_handle = Arc::clone(&root_thread_handle);
529
530            let waiter = std::thread::spawn(move || {
531                let guard = event_waiter.begin_wait();
532                let _ = guard.block_until(None, zx::MonotonicInstant::INFINITE);
533            });
534
535            while event.futex.load(Ordering::Relaxed) != WAITING {
536                std::thread::yield_now();
537            }
538
539            let assigner = std::thread::spawn(move || {
540                barrier_assign.wait();
541                let _ = event_assign.assign_new_owner(&thread_handle);
542            });
543
544            let waker = std::thread::spawn(move || {
545                barrier_waker.wait();
546                event_waker.notify();
547            });
548
549            assigner.join().unwrap();
550            waker.join().unwrap();
551            waiter.join().unwrap();
552        }
553    }
554
555    #[test]
556    fn futex_ownership_is_transferred_when_requeued_before_block() {
557        let event = InterruptibleEvent::new();
558
559        let (root_thread_handle, root_thread_koid) = fuchsia_runtime::with_thread_self(|thread| {
560            (
561                Arc::new(thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap()),
562                thread.koid().unwrap(),
563            )
564        });
565
566        let event_for_blocked_thread = Arc::clone(&event);
567        let (ready_tx, ready_rx) = std::sync::mpsc::channel();
568        let (go_tx, go_rx) = std::sync::mpsc::channel();
569        let blocked_thread = std::thread::spawn(move || {
570            let guard = event_for_blocked_thread.begin_wait();
571            ready_tx.send(()).unwrap();
572            go_rx.recv().unwrap();
573            guard.block_until(None, zx::MonotonicInstant::INFINITE).unwrap();
574        });
575
576        // Call `assign_new_owner` after `begin_wait()`, before the thread calls `block_until()`.
577        ready_rx.recv().unwrap();
578        event.assign_new_owner(&root_thread_handle).unwrap();
579        go_tx.send(()).unwrap();
580
581        while event.get_owner() != Some(root_thread_koid) {
582            std::thread::sleep(std::time::Duration::from_millis(5));
583        }
584
585        event.notify();
586        blocked_thread.join().unwrap();
587    }
588
589    #[test]
590    fn test_wait_clean_stack_then_notify() {
591        let event = InterruptibleEvent::new();
592        let guard = event.begin_wait();
593
594        let other_event = Arc::clone(&event);
595        let thread = std::thread::spawn(move || {
596            std::thread::sleep(std::time::Duration::from_millis(30));
597            other_event.notify();
598        });
599
600        guard
601            .block_until_with_clean_delay(
602                None,
603                zx::MonotonicInstant::INFINITE,
604                zx::MonotonicDuration::from_millis(10),
605            )
606            .expect("failed to be notified after clean_stack");
607        thread.join().expect("failed to join thread");
608    }
609
610    #[test]
611    fn test_wait_clean_stack_then_timeout() {
612        let event = InterruptibleEvent::new();
613        let guard = event.begin_wait();
614        let deadline = zx::MonotonicInstant::after(zx::MonotonicDuration::from_millis(30));
615
616        let result = guard.block_until_with_clean_delay(
617            None,
618            deadline,
619            zx::MonotonicDuration::from_millis(10),
620        );
621        assert_eq!(result, Err(WakeReason::DeadlineExpired));
622        assert!(zx::MonotonicInstant::get() >= deadline);
623    }
624
625    #[test]
626    fn futex_requeue_owner_is_preserved_across_clean_stack() {
627        let event = InterruptibleEvent::new();
628
629        let (root_thread_handle, root_thread_koid) = fuchsia_runtime::with_thread_self(|thread| {
630            (
631                Arc::new(thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap()),
632                thread.koid().unwrap(),
633            )
634        });
635
636        let event_for_blocked_thread = Arc::clone(&event);
637        let (ready_tx, ready_rx) = std::sync::mpsc::channel();
638        let (go_tx, go_rx) = std::sync::mpsc::channel();
639        let blocked_thread = std::thread::spawn(move || {
640            let thread_handle = fuchsia_runtime::with_thread_self(|thread| {
641                thread.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap()
642            });
643            let guard = event_for_blocked_thread.begin_wait();
644            ready_tx.send(thread_handle).unwrap();
645            go_rx.recv().unwrap();
646            guard
647                .block_until_with_clean_delay(
648                    None,
649                    zx::MonotonicInstant::INFINITE,
650                    zx::MonotonicDuration::from_millis(25),
651                )
652                .unwrap();
653        });
654
655        // Assign PI ownership after `begin_wait()` before `blocked_thread` enters
656        // `block_until_with_clean_delay`, so the 25ms `clean_deadline` cannot expire before
657        // `assign_new_owner` runs even on a loaded bot.
658        let blocked_thread_handle = ready_rx.recv().unwrap();
659        event.assign_new_owner(&root_thread_handle).unwrap();
660        go_tx.send(()).unwrap();
661
662        while event.get_owner() != Some(root_thread_koid) {
663            std::thread::sleep(std::time::Duration::from_millis(5));
664        }
665
666        // Sleep past the 25ms clean_deadline so the blocked thread wakes, calls `clean_stack()`,
667        // and re-waits on `requeue_target`.
668        std::thread::sleep(std::time::Duration::from_millis(50));
669        while blocked_thread_handle.info().unwrap().state
670            != zx::ThreadState::Blocked(zx::ThreadBlockType::Futex)
671        {
672            std::thread::sleep(std::time::Duration::from_millis(5));
673        }
674        assert_eq!(event.get_owner(), Some(root_thread_koid));
675
676        event.notify();
677        blocked_thread.join().unwrap();
678    }
679}