Skip to main content

day_async/
lib.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! day-async: the std-only async support parts (docs/async.md).
5//!
6//! Day runs futures on its own main-loop executor (`day::task`) and never brings in an async
7//! runtime. What a part needs beside that executor is small and was, until this crate, written
8//! once per part: a **oneshot** future that a completion arriving on any thread resolves, and a
9//! **token registry** that hands a platform a plain number and looks the waiting closure up when
10//! the number comes back. Both live here, with the locking rules the copies converged on:
11//! deliver outside the lock, and treat an unknown token as a no-op.
12
13use std::collections::BTreeMap;
14use std::future::Future;
15use std::pin::Pin;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::{Arc, Mutex, MutexGuard};
18use std::task::{Context, Poll, Waker};
19
20// ---------------------------------------------------------------------------
21// Oneshot
22// ---------------------------------------------------------------------------
23
24struct Slot<T> {
25    value: Option<T>,
26    waker: Option<Waker>,
27    /// The sender is gone (delivered or dropped); with `value` empty that is [`Dropped`].
28    closed: bool,
29}
30
31fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
32    // A poisoned slot holds nothing a later poll could misread: the flags below are set last.
33    m.lock().unwrap_or_else(|p| p.into_inner())
34}
35
36/// The sending half went away without delivering. A [`Oneshot`] resolves to this rather than
37/// pending forever, so a completion that never comes is an error the caller sees.
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub struct Dropped;
40
41impl std::fmt::Display for Dropped {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.write_str("the completion was dropped before it delivered")
44    }
45}
46
47impl std::error::Error for Dropped {}
48
49/// The sending half of a [`oneshot`]. `Send` whenever `T` is, so a platform's completion thread
50/// can hold it; delivering wakes the receiver outside the lock.
51pub struct Deliver<T> {
52    slot: Arc<Mutex<Slot<T>>>,
53    sent: bool,
54}
55
56impl<T> Deliver<T> {
57    /// Resolve the receiver with `value`. A second delivery is impossible by construction: this
58    /// consumes the sender.
59    pub fn send(mut self, value: T) {
60        self.sent = true;
61        let waker = {
62            let mut s = lock(&self.slot);
63            s.value = Some(value);
64            s.closed = true;
65            s.waker.take()
66        };
67        if let Some(w) = waker {
68            w.wake();
69        }
70    }
71}
72
73impl<T> Drop for Deliver<T> {
74    fn drop(&mut self) {
75        if self.sent {
76            return;
77        }
78        let waker = {
79            let mut s = lock(&self.slot);
80            s.closed = true;
81            s.waker.take()
82        };
83        if let Some(w) = waker {
84            w.wake();
85        }
86    }
87}
88
89/// The receiving half of a [`oneshot`]: a future resolving to the delivered value, or to
90/// [`Dropped`] when the sender went away first. Awaitable from any executor: `day::task`, or a
91/// test's park/unpark `block_on`.
92pub struct Oneshot<T> {
93    slot: Arc<Mutex<Slot<T>>>,
94}
95
96impl<T> Future for Oneshot<T> {
97    type Output = Result<T, Dropped>;
98
99    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
100        let mut s = lock(&self.slot);
101        if let Some(v) = s.value.take() {
102            return Poll::Ready(Ok(v));
103        }
104        if s.closed {
105            return Poll::Ready(Err(Dropped));
106        }
107        s.waker = Some(cx.waker().clone());
108        Poll::Pending
109    }
110}
111
112impl<T> Oneshot<T> {
113    /// Whether a value has arrived (or the sender is gone), without consuming it.
114    pub fn is_ready(&self) -> bool {
115        let s = lock(&self.slot);
116        s.value.is_some() || s.closed
117    }
118}
119
120/// A single-value channel between one completion and one future.
121pub fn oneshot<T>() -> (Deliver<T>, Oneshot<T>) {
122    let slot = Arc::new(Mutex::new(Slot {
123        value: None,
124        waker: None,
125        closed: false,
126    }));
127    (
128        Deliver {
129            slot: slot.clone(),
130            sent: false,
131        },
132        Oneshot { slot },
133    )
134}
135
136// ---------------------------------------------------------------------------
137// Token registry
138// ---------------------------------------------------------------------------
139
140/// The process-wide token counter, shared by every registry so a token identifies one waiting
141/// closure anywhere in the process. Starts at 1: zero is "no token" in every foreign spelling.
142static NEXT_TOKEN: AtomicU64 = AtomicU64::new(1);
143
144/// A fresh token, never reused for the life of the process.
145pub fn next_token() -> u64 {
146    NEXT_TOKEN.fetch_add(1, Ordering::Relaxed)
147}
148
149type Callback<T> = Box<dyn FnOnce(T) + Send>;
150
151/// Waiting closures keyed by token: the shape behind every platform completion that crosses
152/// an FFI boundary as a number (docs/bridge.md "Callbacks").
153///
154/// Rules that make it safe to hand a token to code in another language:
155///
156/// - **Register before the call goes out.** A platform may complete synchronously, or fail
157///   before returning; a slot that exists first is found either way.
158/// - **An unknown token is a no-op.** A late, duplicate, or cancelled completion finds nothing
159///   and does nothing.
160/// - **Deliver outside the lock.** The closure may register a new token or drop a handle; the
161///   registry is never held while it runs.
162///
163/// `const`-constructible, so a generated `static` can own one per declaration.
164pub struct TokenRegistry<T> {
165    slots: Mutex<BTreeMap<u64, Callback<T>>>,
166}
167
168impl<T> Default for TokenRegistry<T> {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174impl<T> TokenRegistry<T> {
175    /// An empty registry.
176    pub const fn new() -> Self {
177        Self {
178            slots: Mutex::new(BTreeMap::new()),
179        }
180    }
181
182    /// Store `cb` under a fresh token.
183    pub fn insert(&self, cb: impl FnOnce(T) + Send + 'static) -> u64 {
184        let token = next_token();
185        lock(&self.slots).insert(token, Box::new(cb));
186        token
187    }
188
189    /// Remove the closure for `token` and run it with `value`. `false` when no closure waits
190    /// under that token (completed, cancelled, or never registered).
191    pub fn complete(&self, token: u64, value: T) -> bool {
192        let cb = lock(&self.slots).remove(&token);
193        match cb {
194            Some(cb) => {
195                cb(value);
196                true
197            }
198            None => false,
199        }
200    }
201
202    /// Forget `token` without running its closure: the waiting side gave up.
203    pub fn remove(&self, token: u64) -> bool {
204        lock(&self.slots).remove(&token).is_some()
205    }
206
207    /// Whether a closure still waits under `token`.
208    pub fn contains(&self, token: u64) -> bool {
209        lock(&self.slots).contains_key(&token)
210    }
211
212    /// How many closures wait, for tests and diagnostics.
213    pub fn len(&self) -> usize {
214        lock(&self.slots).len()
215    }
216
217    /// Whether nothing waits.
218    pub fn is_empty(&self) -> bool {
219        self.len() == 0
220    }
221}
222
223// ---------------------------------------------------------------------------
224// Timers
225// ---------------------------------------------------------------------------
226
227/// A scheduled timer, for [`unschedule`].
228pub type TimerId = u64;
229
230#[cfg(not(target_arch = "wasm32"))]
231mod timer_thread {
232    use std::cmp::Reverse;
233    use std::collections::{BinaryHeap, HashMap};
234    use std::sync::{Condvar, Mutex, OnceLock};
235    use std::time::{Duration, Instant};
236
237    use super::{TimerId, lock};
238
239    type Job = Box<dyn FnOnce() + Send>;
240
241    struct State {
242        heap: BinaryHeap<Reverse<(Instant, TimerId)>>,
243        jobs: HashMap<TimerId, Job>,
244        next: TimerId,
245        started: bool,
246    }
247
248    struct Timers {
249        state: Mutex<State>,
250        wake: Condvar,
251    }
252
253    fn timers() -> &'static Timers {
254        static TIMERS: OnceLock<Timers> = OnceLock::new();
255        TIMERS.get_or_init(|| Timers {
256            state: Mutex::new(State {
257                heap: BinaryHeap::new(),
258                jobs: HashMap::new(),
259                next: 0,
260                started: false,
261            }),
262            wake: Condvar::new(),
263        })
264    }
265
266    pub(super) fn schedule(delay: Duration, job: impl FnOnce() + Send + 'static) -> TimerId {
267        let t = timers();
268        let mut st = lock(&t.state);
269        st.next += 1;
270        let id = st.next;
271        st.heap.push(Reverse((Instant::now() + delay, id)));
272        st.jobs.insert(id, Box::new(job));
273        if !st.started {
274            st.started = std::thread::Builder::new()
275                .name("day-timers".into())
276                .spawn(run)
277                .is_ok();
278        }
279        t.wake.notify_one();
280        id
281    }
282
283    pub(super) fn unschedule(id: TimerId) {
284        lock(&timers().state).jobs.remove(&id);
285    }
286
287    fn run() {
288        let t = timers();
289        let mut st = lock(&t.state);
290        loop {
291            let Some(Reverse((at, id))) = st.heap.peek().copied() else {
292                st = t.wake.wait(st).unwrap_or_else(|p| p.into_inner());
293                continue;
294            };
295            let now = Instant::now();
296            if at > now {
297                st = t
298                    .wake
299                    .wait_timeout(st, at - now)
300                    .map(|(g, _)| g)
301                    .unwrap_or_else(|p| p.into_inner().0);
302                continue;
303            }
304            st.heap.pop();
305            if let Some(job) = st.jobs.remove(&id) {
306                drop(st);
307                job();
308                st = lock(&t.state);
309            }
310        }
311    }
312}
313
314/// Run `job` after `delay` on the process's one timer thread, which starts with the first
315/// timer. Parts use it for limits and backoff instead of parking a thread per wait. Jobs run one
316/// at a time, so a job should hand longer work elsewhere. The browser has no threads: there a
317/// timer never fires, and web code keeps its own clock.
318pub fn schedule(delay: std::time::Duration, job: impl FnOnce() + Send + 'static) -> TimerId {
319    #[cfg(not(target_arch = "wasm32"))]
320    {
321        timer_thread::schedule(delay, job)
322    }
323    #[cfg(target_arch = "wasm32")]
324    {
325        let _ = (delay, job);
326        0
327    }
328}
329
330/// Forget a timer that has not fired. A timer that already fired, or an unknown id, is a no-op.
331pub fn unschedule(id: TimerId) {
332    #[cfg(not(target_arch = "wasm32"))]
333    timer_thread::unschedule(id);
334    #[cfg(target_arch = "wasm32")]
335    let _ = id;
336}
337
338#[cfg(all(test, not(target_arch = "wasm32")))]
339mod timer_tests {
340    use std::sync::mpsc;
341    use std::time::{Duration, Instant};
342
343    #[test]
344    fn timers_fire_in_order_and_unscheduled_ones_never_do() {
345        let (tx, rx) = mpsc::channel();
346        let start = Instant::now();
347        let second = tx.clone();
348        let never = tx.clone();
349        super::schedule(Duration::from_millis(60), move || second.send(2).unwrap());
350        super::schedule(Duration::from_millis(20), move || tx.send(1).unwrap());
351        let dropped = super::schedule(Duration::from_millis(40), move || never.send(9).unwrap());
352        super::unschedule(dropped);
353        assert_eq!(rx.recv_timeout(Duration::from_secs(2)), Ok(1));
354        assert_eq!(rx.recv_timeout(Duration::from_secs(2)), Ok(2));
355        assert!(rx.recv_timeout(Duration::from_millis(100)).is_err());
356        assert!(start.elapsed() >= Duration::from_millis(60));
357    }
358}
359
360// ---------------------------------------------------------------------------
361// Blocking pool
362// ---------------------------------------------------------------------------
363
364/// Why a [`Blocking`] future resolved without its job's value.
365#[derive(Clone, Copy, Debug, PartialEq, Eq)]
366pub enum BlockingError {
367    /// The job panicked. The panic went to the panic hook like any other; the pool thread that
368    /// ran it carries on with the next job.
369    Panicked,
370    /// The job never ran: the pool could not start its first thread.
371    Closed,
372}
373
374impl std::fmt::Display for BlockingError {
375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376        f.write_str(match self {
377            BlockingError::Panicked => "the blocking job panicked",
378            BlockingError::Closed => "the blocking job never ran: no pool thread could start",
379        })
380    }
381}
382
383impl std::error::Error for BlockingError {}
384
385/// The future [`spawn_blocking`] returns: the job's value once a pool thread has run it.
386///
387/// Dropping it before the job starts cancels the job, which then never runs. Dropping it while
388/// the job runs lets the job finish on its thread and discards the value: a blocking call has no
389/// way to be interrupted. [`Blocking::detach`] keeps the job without the value. Awaitable from
390/// any executor, `day::task` included.
391#[must_use = "await the future to receive the job's value, or `detach` it"]
392pub struct Blocking<T> {
393    inner: BlockingInner<T>,
394}
395
396enum BlockingInner<T> {
397    /// Queued for the pool; the value arrives through the oneshot.
398    #[cfg(not(target_arch = "wasm32"))]
399    Pooled {
400        receiver: Oneshot<Result<T, BlockingError>>,
401        cancelled: Arc<std::sync::atomic::AtomicBool>,
402        detached: bool,
403    },
404    /// No thread will run it (the web, or a synchronous test harness): it runs at the first
405    /// poll, on the polling thread. `Send` so the future is, like the pooled one.
406    Inline(Option<Box<dyn FnOnce() -> T + Send>>),
407}
408
409impl<T> Future for Blocking<T> {
410    type Output = Result<T, BlockingError>;
411
412    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
413        // SAFETY: `inner` is never moved out of the pinned future; the oneshot is `Unpin`.
414        let inner = unsafe { &mut self.get_unchecked_mut().inner };
415        // An inline job needs no waker: it is ready at this poll.
416        let _ = &cx;
417        match inner {
418            #[cfg(not(target_arch = "wasm32"))]
419            BlockingInner::Pooled { receiver, .. } => Pin::new(receiver)
420                .poll(cx)
421                .map(|r| r.unwrap_or(Err(BlockingError::Closed))),
422            // `catch_unwind` is a plain call under the web's abort-on-panic; it does its job on
423            // the targets that unwind.
424            BlockingInner::Inline(job) => Poll::Ready(match job.take() {
425                Some(job) => std::panic::catch_unwind(std::panic::AssertUnwindSafe(job))
426                    .map_err(|_| BlockingError::Panicked),
427                None => Err(BlockingError::Closed),
428            }),
429        }
430    }
431}
432
433impl<T> Blocking<T> {
434    /// Let the job run to completion without waiting for its value: for work whose effect is
435    /// its own completion callback. A pooled job keeps its queue slot; an inline one runs now.
436    pub fn detach(mut self) {
437        match &mut self.inner {
438            #[cfg(not(target_arch = "wasm32"))]
439            BlockingInner::Pooled { detached, .. } => *detached = true,
440            BlockingInner::Inline(job) => {
441                if let Some(job) = job.take() {
442                    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(job));
443                }
444            }
445        }
446    }
447}
448
449#[cfg(not(target_arch = "wasm32"))]
450impl<T> Drop for Blocking<T> {
451    fn drop(&mut self) {
452        if let BlockingInner::Pooled {
453            cancelled,
454            detached: false,
455            ..
456        } = &self.inner
457        {
458            cancelled.store(true, std::sync::atomic::Ordering::Release);
459        }
460    }
461}
462
463/// Run `job` on a pool thread and resolve to its value on the awaiting side.
464///
465/// The one bridge from an `async` flow to a call that blocks or computes for a while: a file
466/// read, a parse, a hash, a database open, a `block_on` into some other runtime. The pool is
467/// process-wide, starts with the first job, holds at most [`blocking_limit`] threads, and lets a
468/// thread go after a few idle seconds; jobs beyond the limit queue in order. Nothing here knows
469/// about Day's executor, so a part can await it under `day::task` or a test's `block_on` alike.
470///
471/// On the web, where no thread exists, the job runs inline at the future's first poll. Keep
472/// web jobs short, or branch on `cfg!(target_arch = "wasm32")` and use the platform's async API.
473/// A synchronous test harness (the mock toolkit) asks for the same with
474/// [`set_blocking_inline`].
475///
476/// ```ignore
477/// let bytes = day::spawn_blocking(move || std::fs::read(&path)).await;
478/// match bytes {
479///     Ok(Ok(bytes)) => contents.set(bytes),
480///     Ok(Err(io)) => status.set(io.to_string()),
481///     Err(e) => status.set(e.to_string()),
482/// }
483/// ```
484pub fn spawn_blocking<T: Send + 'static>(job: impl FnOnce() -> T + Send + 'static) -> Blocking<T> {
485    if blocking_inline() {
486        return Blocking {
487            inner: BlockingInner::Inline(Some(Box::new(job))),
488        };
489    }
490    #[cfg(not(target_arch = "wasm32"))]
491    {
492        use std::sync::atomic::{AtomicBool, Ordering};
493        let (tx, rx) = oneshot();
494        let cancelled = Arc::new(AtomicBool::new(false));
495        let flag = cancelled.clone();
496        let run = move || {
497            // The awaiting side is gone: skip the work. (Dropping `tx` here is unobserved.)
498            if flag.load(Ordering::Acquire) {
499                return;
500            }
501            let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(job));
502            tx.send(out.map_err(|_| BlockingError::Panicked));
503        };
504        // A refused job drops its `tx` with the box, so the receiver resolves to `Closed`.
505        blocking_pool::submit(Box::new(run));
506        Blocking {
507            inner: BlockingInner::Pooled {
508                receiver: rx,
509                cancelled,
510                detached: false,
511            },
512        }
513    }
514    #[cfg(target_arch = "wasm32")]
515    {
516        unreachable!("the web always runs blocking jobs inline")
517    }
518}
519
520static BLOCKING_INLINE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
521
522/// Make every later [`spawn_blocking`] run its job inline at the first poll, as the web does,
523/// instead of on a pool thread. For a synchronous test harness whose main-loop posts run on
524/// the calling thread (the mock toolkit), where a wake from a pool thread would have no loop
525/// to land on. Process-wide; the web ignores it (always inline).
526pub fn set_blocking_inline(inline: bool) {
527    BLOCKING_INLINE.store(inline, std::sync::atomic::Ordering::Release);
528}
529
530/// Whether [`spawn_blocking`] runs jobs inline: always on the web, else per
531/// [`set_blocking_inline`].
532pub fn blocking_inline() -> bool {
533    cfg!(target_arch = "wasm32") || BLOCKING_INLINE.load(std::sync::atomic::Ordering::Acquire)
534}
535
536/// How many threads the blocking pool may hold at once: the machine's parallelism, at least
537/// four (a job waiting behind three slow ones still gets a thread), at most thirty-two. Zero on
538/// the web, where jobs run inline.
539pub fn blocking_limit() -> usize {
540    #[cfg(not(target_arch = "wasm32"))]
541    {
542        blocking_pool::limit()
543    }
544    #[cfg(target_arch = "wasm32")]
545    {
546        0
547    }
548}
549
550/// How many pool threads exist right now, idle ones included: for diagnostics and tests.
551pub fn blocking_threads() -> usize {
552    #[cfg(not(target_arch = "wasm32"))]
553    {
554        blocking_pool::threads()
555    }
556    #[cfg(target_arch = "wasm32")]
557    {
558        0
559    }
560}
561
562#[cfg(not(target_arch = "wasm32"))]
563mod blocking_pool {
564    use std::collections::VecDeque;
565    use std::sync::{Condvar, Mutex, OnceLock};
566    use std::time::Duration;
567
568    use super::lock;
569
570    type Job = Box<dyn FnOnce() + Send>;
571
572    /// How long a thread with nothing to do waits for a job before it exits.
573    const IDLE_FOR: Duration = Duration::from_secs(2);
574
575    struct State {
576        queue: VecDeque<Job>,
577        /// Threads alive, working or waiting.
578        threads: usize,
579        /// Threads waiting on `wake`.
580        idle: usize,
581    }
582
583    struct Pool {
584        state: Mutex<State>,
585        wake: Condvar,
586        limit: usize,
587    }
588
589    fn pool() -> &'static Pool {
590        static POOL: OnceLock<Pool> = OnceLock::new();
591        POOL.get_or_init(|| Pool {
592            state: Mutex::new(State {
593                queue: VecDeque::new(),
594                threads: 0,
595                idle: 0,
596            }),
597            wake: Condvar::new(),
598            limit: std::thread::available_parallelism()
599                .map_or(4, |n| n.get())
600                .clamp(4, 32),
601        })
602    }
603
604    pub(super) fn limit() -> usize {
605        pool().limit
606    }
607
608    pub(super) fn threads() -> usize {
609        lock(&pool().state).threads
610    }
611
612    /// Queue `job`; start a thread when no idle one will take it and the limit allows. A job
613    /// that no thread can ever run (the first thread failed to start) is dropped, which drops
614    /// its delivery.
615    pub(super) fn submit(job: Job) {
616        let p = pool();
617        let mut st = lock(&p.state);
618        st.queue.push_back(job);
619        // A wake lost to a race costs latency, not the job: a woken thread drains the queue
620        // before it waits again, and the spawn below covers the queue outgrowing the waiters.
621        if st.queue.len() > st.idle && st.threads < p.limit {
622            match std::thread::Builder::new()
623                .name("day-blocking".into())
624                .spawn(worker)
625            {
626                Ok(_) => st.threads += 1,
627                Err(_) if st.threads == 0 => {
628                    st.queue.pop_back();
629                    return;
630                }
631                // Other threads exist; they will get to it.
632                Err(_) => {}
633            }
634        }
635        p.wake.notify_one();
636    }
637
638    fn worker() {
639        let p = pool();
640        let mut st = lock(&p.state);
641        loop {
642            while let Some(job) = st.queue.pop_front() {
643                drop(st);
644                job();
645                st = lock(&p.state);
646            }
647            st.idle += 1;
648            let (guard, waited) = p
649                .wake
650                .wait_timeout(st, IDLE_FOR)
651                .unwrap_or_else(|poisoned| poisoned.into_inner());
652            st = guard;
653            st.idle -= 1;
654            if waited.timed_out() && st.queue.is_empty() {
655                st.threads -= 1;
656                return;
657            }
658        }
659    }
660}
661
662#[cfg(all(test, not(target_arch = "wasm32")))]
663mod blocking_tests {
664    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
665    use std::sync::{Arc, mpsc};
666    use std::time::Duration;
667
668    use super::tests::block_on;
669    use super::{BlockingError, blocking_limit, blocking_threads, spawn_blocking};
670
671    /// Inline mode is process-wide and the pool is shared, so these tests take turns: one
672    /// toggling inline mode under another's thread assertion would fail it.
673    fn turn() -> std::sync::MutexGuard<'static, ()> {
674        static TURN: std::sync::Mutex<()> = std::sync::Mutex::new(());
675        TURN.lock().unwrap_or_else(|p| p.into_inner())
676    }
677
678    #[test]
679    fn a_job_runs_on_another_thread_and_delivers_its_value() {
680        let _turn = turn();
681        let here = std::thread::current().id();
682        let out = block_on(spawn_blocking(move || {
683            (std::thread::current().id() != here, 6 * 7)
684        }));
685        assert_eq!(out, Ok((true, 42)));
686    }
687
688    #[test]
689    fn a_panicking_job_is_an_error_and_the_pool_carries_on() {
690        let _turn = turn();
691        let out: Result<(), _> = block_on(spawn_blocking(|| panic!("job failed")));
692        assert_eq!(out, Err(BlockingError::Panicked));
693        assert_eq!(block_on(spawn_blocking(|| 1)), Ok(1));
694    }
695
696    #[test]
697    fn a_future_dropped_before_its_job_starts_cancels_the_job() {
698        let _turn = turn();
699        // Hold every pool thread so the marker job has to queue.
700        let limit = blocking_limit();
701        let (release, held) = mpsc::channel::<()>();
702        let held = Arc::new(std::sync::Mutex::new(held));
703        let started = Arc::new(AtomicUsize::new(0));
704        let holders: Vec<_> = (0..limit)
705            .map(|_| {
706                let held = held.clone();
707                let started = started.clone();
708                spawn_blocking(move || {
709                    started.fetch_add(1, Ordering::SeqCst);
710                    // One receiver, shared: each holder takes one release.
711                    let _ = held.lock().unwrap().recv();
712                })
713            })
714            .collect();
715        while started.load(Ordering::SeqCst) < limit {
716            std::thread::sleep(Duration::from_millis(1));
717        }
718        let ran = Arc::new(AtomicBool::new(false));
719        let marker = {
720            let ran = ran.clone();
721            spawn_blocking(move || ran.store(true, Ordering::SeqCst))
722        };
723        drop(marker);
724        for _ in 0..limit {
725            release.send(()).unwrap();
726        }
727        for h in holders {
728            assert_eq!(block_on(h), Ok(()));
729        }
730        // Every holder finished, so the queue behind them has drained too.
731        assert_eq!(block_on(spawn_blocking(|| ())), Ok(()));
732        assert!(!ran.load(Ordering::SeqCst), "a cancelled job ran");
733    }
734
735    #[test]
736    fn many_jobs_complete_within_the_thread_limit() {
737        let _turn = turn();
738        let limit = blocking_limit();
739        let live = Arc::new(AtomicUsize::new(0));
740        let peak = Arc::new(AtomicUsize::new(0));
741        let jobs: Vec<_> = (0..limit * 8)
742            .map(|i| {
743                let (live, peak) = (live.clone(), peak.clone());
744                spawn_blocking(move || {
745                    let now = live.fetch_add(1, Ordering::SeqCst) + 1;
746                    peak.fetch_max(now, Ordering::SeqCst);
747                    std::thread::sleep(Duration::from_millis(2));
748                    live.fetch_sub(1, Ordering::SeqCst);
749                    i
750                })
751            })
752            .collect();
753        let mut sum = 0;
754        for j in jobs {
755            sum += block_on(j).unwrap();
756        }
757        assert_eq!(sum, (0..limit * 8).sum::<usize>());
758        assert!(peak.load(Ordering::SeqCst) <= limit, "{peak:?} > {limit}");
759        assert!(blocking_threads() <= limit);
760    }
761
762    #[test]
763    fn the_future_is_send_for_a_send_value() {
764        let _turn = turn();
765        fn assert_send<T: Send>(_: &T) {}
766        let fut = spawn_blocking(|| String::from("sent"));
767        assert_send(&fut);
768        assert_eq!(block_on(fut), Ok(String::from("sent")));
769    }
770
771    #[test]
772    fn a_detached_job_still_runs() {
773        let _turn = turn();
774        let (tx, rx) = mpsc::channel();
775        spawn_blocking(move || tx.send(5).unwrap()).detach();
776        assert_eq!(rx.recv_timeout(Duration::from_secs(5)), Ok(5));
777    }
778
779    #[test]
780    fn an_inline_job_runs_at_the_first_poll_on_the_polling_thread() {
781        let _turn = turn();
782        let here = std::thread::current().id();
783        super::set_blocking_inline(true);
784        let fut = spawn_blocking(move || std::thread::current().id() == here);
785        let detached = spawn_blocking(|| ());
786        super::set_blocking_inline(false);
787        assert_eq!(block_on(fut), Ok(true));
788        detached.detach();
789    }
790
791    #[test]
792    fn idle_threads_leave_after_a_while() {
793        let _turn = turn();
794        assert_eq!(block_on(spawn_blocking(|| ())), Ok(()));
795        assert!(blocking_threads() >= 1);
796        // Other tests in this binary may keep the pool busy; wait for a quiet span.
797        let deadline = std::time::Instant::now() + Duration::from_secs(12);
798        while blocking_threads() > 0 && std::time::Instant::now() < deadline {
799            std::thread::sleep(Duration::from_millis(100));
800        }
801        assert_eq!(blocking_threads(), 0, "idle pool threads did not exit");
802    }
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808    use std::sync::Arc as StdArc;
809    use std::task::Wake;
810
811    /// The ~20-line park/unpark executor every part's future tests use.
812    pub(super) fn block_on<F: Future>(mut fut: F) -> F::Output {
813        struct Unpark(std::thread::Thread);
814        impl Wake for Unpark {
815            fn wake(self: StdArc<Self>) {
816                self.0.unpark();
817            }
818        }
819        let waker = Waker::from(StdArc::new(Unpark(std::thread::current())));
820        let mut cx = Context::from_waker(&waker);
821        // SAFETY: `fut` lives on this stack frame and is never moved after being pinned.
822        let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
823        loop {
824            if let Poll::Ready(v) = fut.as_mut().poll(&mut cx) {
825                return v;
826            }
827            std::thread::park();
828        }
829    }
830
831    #[test]
832    fn delivers_across_a_thread() {
833        let (tx, rx) = oneshot::<u32>();
834        std::thread::spawn(move || tx.send(7));
835        assert_eq!(block_on(rx), Ok(7));
836    }
837
838    #[test]
839    fn a_dropped_sender_resolves_rather_than_hangs() {
840        let (tx, rx) = oneshot::<u32>();
841        drop(tx);
842        assert_eq!(block_on(rx), Err(Dropped));
843    }
844
845    #[test]
846    fn a_value_sent_before_the_first_poll_is_ready_at_once() {
847        let (tx, rx) = oneshot::<&str>();
848        tx.send("now");
849        assert!(rx.is_ready());
850        assert_eq!(block_on(rx), Ok("now"));
851    }
852
853    static REG: TokenRegistry<i32> = TokenRegistry::new();
854
855    #[test]
856    fn a_token_completes_exactly_once() {
857        let hits = StdArc::new(Mutex::new(Vec::new()));
858        let h = hits.clone();
859        let token = REG.insert(move |v| h.lock().unwrap().push(v));
860        assert!(REG.contains(token));
861        assert!(REG.complete(token, 1));
862        assert!(!REG.complete(token, 2), "a second completion finds nothing");
863        assert_eq!(*hits.lock().unwrap(), vec![1]);
864    }
865
866    #[test]
867    fn a_removed_token_never_runs() {
868        let token = REG.insert(|_| panic!("cancelled closures never run"));
869        assert!(REG.remove(token));
870        assert!(!REG.complete(token, 0));
871    }
872
873    #[test]
874    fn a_completion_may_register_again_from_inside_the_closure() {
875        // Would deadlock if the registry were held while the closure runs.
876        let token = REG.insert(|_| {
877            let _ = REG.insert(|_| {});
878        });
879        assert!(REG.complete(token, 0));
880    }
881
882    #[test]
883    fn tokens_are_process_unique() {
884        let a = next_token();
885        let b = next_token();
886        assert!(b > a);
887        assert_ne!(a, 0);
888    }
889}