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}