1use 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
20struct Slot<T> {
25 value: Option<T>,
26 waker: Option<Waker>,
27 closed: bool,
29}
30
31fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
32 m.lock().unwrap_or_else(|p| p.into_inner())
34}
35
36#[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
49pub struct Deliver<T> {
52 slot: Arc<Mutex<Slot<T>>>,
53 sent: bool,
54}
55
56impl<T> Deliver<T> {
57 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
89pub 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 pub fn is_ready(&self) -> bool {
115 let s = lock(&self.slot);
116 s.value.is_some() || s.closed
117 }
118}
119
120pub 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
136static NEXT_TOKEN: AtomicU64 = AtomicU64::new(1);
143
144pub fn next_token() -> u64 {
146 NEXT_TOKEN.fetch_add(1, Ordering::Relaxed)
147}
148
149type Callback<T> = Box<dyn FnOnce(T) + Send>;
150
151pub 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 pub const fn new() -> Self {
177 Self {
178 slots: Mutex::new(BTreeMap::new()),
179 }
180 }
181
182 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 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 pub fn remove(&self, token: u64) -> bool {
204 lock(&self.slots).remove(&token).is_some()
205 }
206
207 pub fn contains(&self, token: u64) -> bool {
209 lock(&self.slots).contains_key(&token)
210 }
211
212 pub fn len(&self) -> usize {
214 lock(&self.slots).len()
215 }
216
217 pub fn is_empty(&self) -> bool {
219 self.len() == 0
220 }
221}
222
223pub 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
314pub 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
330pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
366pub enum BlockingError {
367 Panicked,
370 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#[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 #[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 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 let inner = unsafe { &mut self.get_unchecked_mut().inner };
415 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 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 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
463pub 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 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 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
522pub fn set_blocking_inline(inline: bool) {
527 BLOCKING_INLINE.store(inline, std::sync::atomic::Ordering::Release);
528}
529
530pub fn blocking_inline() -> bool {
533 cfg!(target_arch = "wasm32") || BLOCKING_INLINE.load(std::sync::atomic::Ordering::Acquire)
534}
535
536pub 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
550pub 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 const IDLE_FOR: Duration = Duration::from_secs(2);
574
575 struct State {
576 queue: VecDeque<Job>,
577 threads: usize,
579 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 pub(super) fn submit(job: Job) {
616 let p = pool();
617 let mut st = lock(&p.state);
618 st.queue.push_back(job);
619 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 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 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 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 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 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 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 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 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 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}