Skip to main content

ntex_util/
task.rs

1//! Task wake-up and scheduling utilities.
2use std::{cell::Cell, fmt, task::Waker};
3
4/// A single-threaded synchronization primitive for task wake-up.
5///
6/// Sometimes the task interested in a given event will change over time.
7/// A `LocalWaker` can coordinate notifications with the consumer
8/// potentially "updating" the underlying task to wake up. This is useful in
9/// scenarios where a computation completes in another task and wants to
10/// notify the consumer, but the consumer is in the process of being migrated to
11/// a new logical task.
12///
13/// Consumers should call `register` before checking the result of a computation
14/// and producers should call `wake` after producing the computation (this
15/// differs from the usual `thread::park` pattern). It is also permitted for
16/// `wake` to be called **before** `register`. This results in a no-op.
17///
18/// A single `LocalWaker` may be reused for any number of calls to `register` or
19/// `wake`.
20///
21/// The slot and its owned local handle must remain on their acquiring thread.
22/// Use [`take`](Self::take) to transfer a registered standard waker instead.
23///
24/// ```compile_fail
25/// fn assert_send<T: Send>() {}
26/// assert_send::<ntex_util::task::LocalWaker>();
27/// ```
28///
29/// ```compile_fail
30/// fn assert_sync<T: Sync>() {}
31/// assert_sync::<ntex_util::task::LocalWaker>();
32/// ```
33#[derive(Default)]
34pub struct LocalWaker {
35    waker: Cell<Option<ntex_rt::LocalWaker>>,
36}
37
38#[cfg(target_pointer_width = "64")]
39const _: () = assert!(size_of::<LocalWaker>() == 16);
40
41impl LocalWaker {
42    /// Creates an empty `LocalWaker`.
43    pub fn new() -> Self {
44        LocalWaker::with(None)
45    }
46
47    /// Creates a `LocalWaker` with an optional initial waker.
48    pub fn with(waker: Option<Waker>) -> Self {
49        LocalWaker {
50            waker: Cell::new(waker.map(ntex_rt::LocalWaker::from)),
51        }
52    }
53
54    #[inline]
55    /// Registers the waker to be notified on calls to `wake`.
56    ///
57    /// Returns `true` if a waker was already registered.
58    ///
59    /// If cloning the replacement panics, the previous registration is retained
60    /// unless the clone callback itself changes the slot.
61    pub fn register(&self, waker: &Waker) -> bool {
62        let prev = self.waker.take();
63        let registered = prev.is_some();
64        let same = prev.as_ref().is_some_and(|prev| prev.will_wake(waker));
65        self.waker.set(prev);
66        if !same {
67            // Clone and drop callbacks may reenter the slot. Keep the old
68            // registration until cloning succeeds, then publish before dropping.
69            let next = ntex_rt::LocalWaker::from_waker(waker);
70            drop(self.waker.replace(Some(next)));
71        }
72        registered
73    }
74
75    #[inline]
76    /// Calls `wake` on the last `Waker` passed to `register`.
77    ///
78    /// The waker is consumed, so a later `wake` does nothing until `register`
79    /// is called again. If no waker is registered, this does nothing.
80    pub fn wake(&self) {
81        if let Some(waker) = self.take_local() {
82            waker.wake();
83        }
84    }
85
86    #[inline]
87    /// Calls `wake` on the last `Waker` passed to `register`.
88    ///
89    /// Returns `false` if no waker is registered. Like [`wake`](Self::wake),
90    /// this consumes the registered waker.
91    pub fn wake_checked(&self) -> bool {
92        if let Some(waker) = self.take_local() {
93            waker.wake();
94            true
95        } else {
96            false
97        }
98    }
99
100    /// Returns the last `Waker` passed to `register`, so that the user can wake it.
101    ///
102    /// This empties the slot and converts the owned handle into a thread-safe
103    /// standard waker without cloning. If no waker is registered, returns `None`.
104    /// Use [`take_local`](Self::take_local) to retain native local operations.
105    pub fn take(&self) -> Option<Waker> {
106        self.take_local().map(ntex_rt::LocalWaker::into_waker)
107    }
108
109    /// Takes the registered wake handle, leaving the slot empty.
110    ///
111    /// The returned handle retains native local operations and cannot be sent
112    /// to another thread. If no waker is registered, returns `None`.
113    #[inline]
114    pub fn take_local(&self) -> Option<ntex_rt::LocalWaker> {
115        self.waker.take()
116    }
117
118    #[doc(hidden)]
119    /// Returns `true` if a waker is registered.
120    pub fn is_set(&self) -> bool {
121        let waker = self.waker.take();
122        let set = waker.is_some();
123        self.waker.set(waker);
124        set
125    }
126}
127
128/// Cloning creates an empty `LocalWaker`; the registered waker is not copied.
129impl Clone for LocalWaker {
130    fn clone(&self) -> Self {
131        LocalWaker::new()
132    }
133}
134
135impl fmt::Debug for LocalWaker {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        write!(f, "LocalWaker")
138    }
139}
140
141/// Yields execution back to the current runtime.
142pub async fn yield_to() {
143    use std::{future::Future, pin::Pin, task::Context, task::Poll};
144
145    struct Yield {
146        completed: bool,
147    }
148
149    impl Future for Yield {
150        type Output = ();
151
152        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
153            if self.completed {
154                return Poll::Ready(());
155            }
156
157            self.completed = true;
158            cx.waker().wake_by_ref();
159
160            Poll::Pending
161        }
162    }
163
164    Yield { completed: false }.await;
165}
166
167#[cfg(test)]
168mod test {
169    use super::*;
170
171    #[ntex::test]
172    async fn yield_test() {
173        yield_to().await;
174    }
175
176    #[ntex::test]
177    async fn local_slot_wakes_current_task() {
178        crate::spawn(async {
179            let slot = LocalWaker::new();
180            let mut polls = 0;
181            std::future::poll_fn(|cx| {
182                polls += 1;
183                if polls == 4 {
184                    return std::task::Poll::Ready(());
185                }
186                assert!(!slot.register(cx.waker()));
187                match polls {
188                    1 => slot.wake(),
189                    2 => slot.take_local().unwrap().wake(),
190                    _ => slot.take().unwrap().wake(),
191                }
192                std::task::Poll::Pending
193            })
194            .await;
195            assert!(!slot.is_set());
196        })
197        .await
198        .unwrap();
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use std::panic::{AssertUnwindSafe, catch_unwind};
205    use std::rc::Rc;
206    use std::sync::Arc;
207    use std::sync::atomic::{AtomicUsize, Ordering};
208    use std::task::{RawWaker, RawWakerVTable, Wake};
209    use std::thread::LocalKey;
210
211    use super::*;
212
213    static CLONES: AtomicUsize = AtomicUsize::new(0);
214
215    static VTABLE: RawWakerVTable = RawWakerVTable::new(
216        |p| {
217            CLONES.fetch_add(1, Ordering::Relaxed);
218            RawWaker::new(p, &VTABLE)
219        },
220        |_| {},
221        |_| {},
222        |_| {},
223    );
224
225    type Hook = Cell<Option<Box<dyn FnOnce()>>>;
226
227    // Waker callbacks may run on any thread; local test closures stay in TLS.
228    thread_local! {
229        static ON_CLONE: Hook = Cell::new(None);
230        static ON_DROP: Hook = Cell::new(None);
231        static ON_WAKE: Hook = Cell::new(None);
232    }
233
234    fn run_hook(key: &'static LocalKey<Hook>) {
235        if let Some(hook) = key.with(Cell::take) {
236            hook();
237        }
238    }
239
240    fn hook_waker() -> Waker {
241        static HOOK_VTABLE: RawWakerVTable = RawWakerVTable::new(
242            |p| {
243                run_hook(&ON_CLONE);
244                RawWaker::new(p, &HOOK_VTABLE)
245            },
246            |_| run_hook(&ON_WAKE),
247            |_| run_hook(&ON_WAKE),
248            |_| run_hook(&ON_DROP),
249        );
250        unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &HOOK_VTABLE)) }
251    }
252
253    #[derive(Default)]
254    struct WakeCounter(AtomicUsize);
255
256    impl Wake for WakeCounter {
257        fn wake(self: Arc<Self>) {
258            self.0.fetch_add(1, Ordering::Relaxed);
259        }
260    }
261
262    fn counted_waker() -> (Arc<WakeCounter>, Waker) {
263        let count = Arc::new(WakeCounter::default());
264        let waker = Waker::from(count.clone());
265        (count, waker)
266    }
267
268    #[test]
269    fn test_register_same_waker() {
270        static A: u8 = 0;
271        static B: u8 = 0;
272        let a = unsafe { Waker::from_raw(RawWaker::new((&raw const A).cast(), &VTABLE)) };
273        let b = unsafe { Waker::from_raw(RawWaker::new((&raw const B).cast(), &VTABLE)) };
274
275        let w = LocalWaker::new();
276        assert!(!w.register(&a));
277        assert!(w.register(&a));
278        assert!(w.register(&a));
279        assert_eq!(CLONES.load(Ordering::Relaxed), 1);
280
281        assert!(w.register(&b));
282        assert_eq!(CLONES.load(Ordering::Relaxed), 2);
283        assert!(w.take().unwrap().will_wake(&b));
284    }
285
286    #[test]
287    fn local_waker_clone_is_empty() {
288        let waker = LocalWaker::new();
289        waker.register(std::task::Waker::noop());
290        assert!(waker.is_set());
291        let cloned = waker.clone();
292        assert!(!cloned.is_set());
293        assert!(!cloned.wake_checked());
294        assert!(waker.is_set());
295        assert_eq!(format!("{waker:?}"), "LocalWaker");
296    }
297
298    #[test]
299    fn owning_conversions_and_wakes_do_not_clone() {
300        ON_CLONE.set(Some(Box::new(|| panic!("unexpected clone"))));
301        let calls = Rc::new(Cell::new(0));
302        for path in 0..4 {
303            let calls = calls.clone();
304            ON_WAKE.set(Some(Box::new(move || calls.set(calls.get() + 1))));
305            let slot = LocalWaker::with(Some(hook_waker()));
306            match path {
307                0 => slot.take().unwrap().wake(),
308                1 => slot.take_local().unwrap().wake(),
309                2 => slot.wake(),
310                _ => assert!(slot.wake_checked()),
311            }
312            assert!(slot.take().is_none());
313            assert!(slot.take_local().is_none());
314            assert!(!slot.wake_checked());
315        }
316        assert_eq!(calls.get(), 4);
317        assert!(ON_CLONE.with(Cell::take).is_some());
318    }
319
320    #[test]
321    fn clone_panic_preserves_registration() {
322        let (calls, original) = counted_waker();
323        let slot = LocalWaker::with(Some(original));
324        let replacement = hook_waker();
325        ON_CLONE.set(Some(Box::new(|| panic!("clone"))));
326        assert!(catch_unwind(AssertUnwindSafe(|| slot.register(&replacement))).is_err());
327        assert!(slot.wake_checked());
328        assert_eq!(calls.0.load(Ordering::Relaxed), 1);
329    }
330
331    #[test]
332    fn clone_reentry_preserves_nested_changes_on_unwind() {
333        for panic in [false, true] {
334            let (previous_calls, previous) = counted_waker();
335            let (nested_calls, nested) = counted_waker();
336            let slot = Rc::new(LocalWaker::with(Some(previous)));
337            let callback_slot = slot.clone();
338            ON_CLONE.set(Some(Box::new(move || {
339                assert!(callback_slot.wake_checked());
340                assert!(!callback_slot.register(&nested));
341                assert!(!panic, "clone");
342            })));
343            let replacement = hook_waker();
344            let result = catch_unwind(AssertUnwindSafe(|| slot.register(&replacement)));
345            if panic {
346                assert!(result.is_err());
347            } else {
348                assert!(result.unwrap());
349            }
350            assert_eq!(previous_calls.0.load(Ordering::Relaxed), 1);
351            assert!(slot.wake_checked());
352            assert_eq!(nested_calls.0.load(Ordering::Relaxed), usize::from(panic));
353        }
354    }
355
356    #[test]
357    fn replaced_drop_reentry_and_unwind() {
358        for panic in [false, true] {
359            let (replacement_calls, replacement) = counted_waker();
360            let (nested_calls, nested) = counted_waker();
361            let slot = Rc::new(LocalWaker::with(Some(hook_waker())));
362            let callback_slot = slot.clone();
363            ON_DROP.set(Some(Box::new(move || {
364                assert!(callback_slot.wake_checked());
365                assert!(!callback_slot.register(&nested));
366                assert!(!panic, "drop");
367            })));
368            let result = catch_unwind(AssertUnwindSafe(|| slot.register(&replacement)));
369            if panic {
370                assert!(result.is_err());
371            } else {
372                assert!(result.unwrap());
373            }
374            assert_eq!(replacement_calls.0.load(Ordering::Relaxed), 1);
375            assert!(slot.wake_checked());
376            assert_eq!(nested_calls.0.load(Ordering::Relaxed), 1);
377        }
378    }
379
380    #[test]
381    fn wake_reentry_and_unwind() {
382        for panic in [false, true] {
383            for checked in [false, true] {
384                let (calls, nested) = counted_waker();
385                let slot = Rc::new(LocalWaker::with(Some(hook_waker())));
386                let callback_slot = slot.clone();
387                ON_WAKE.set(Some(Box::new(move || {
388                    assert!(!callback_slot.is_set());
389                    assert!(!callback_slot.register(&nested));
390                    assert!(!panic, "wake");
391                })));
392                let result = catch_unwind(AssertUnwindSafe(|| {
393                    if checked {
394                        assert!(slot.wake_checked());
395                    } else {
396                        slot.wake();
397                    }
398                }));
399                assert_eq!(result.is_err(), panic);
400                assert!(slot.wake_checked());
401                assert_eq!(calls.0.load(Ordering::Relaxed), 1);
402            }
403        }
404    }
405
406    #[test]
407    fn native_slot_delivery_and_shutdown() {
408        #[derive(Debug)]
409        struct NoopNotify;
410
411        impl ntex_rt::Notify for NoopNotify {
412            fn notify(&self) -> std::io::Result<()> {
413                Ok(())
414            }
415        }
416
417        let rt = ntex_rt::Runtime::new(Box::new(NoopNotify));
418        let slot = Rc::new(LocalWaker::new());
419        let polls = Rc::new(Cell::new(0));
420        let (task_slot, task_polls) = (slot.clone(), polls.clone());
421        let task = rt.spawn(std::future::poll_fn(move |cx| {
422            task_polls.set(task_polls.get() + 1);
423            task_slot.register(cx.waker());
424            std::task::Poll::<()>::Pending
425        }));
426        rt.poll();
427        slot.take_local().unwrap().wake();
428        rt.poll();
429        let standard = slot.take().unwrap();
430        std::thread::spawn(move || standard.wake()).join().unwrap();
431        rt.poll();
432        assert_eq!(polls.get(), 3);
433        assert_eq!(Rc::strong_count(&slot), 2);
434        drop(rt);
435        assert!(task.is_finished());
436        assert_eq!(Rc::strong_count(&slot), 1);
437        assert!(slot.wake_checked());
438        assert!(!slot.wake_checked());
439    }
440}