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}