coven_foundation/
bounded_workers.rs1use std::num::NonZeroUsize;
4use std::sync::{Arc, Mutex};
5use tokio::sync::{mpsc, oneshot};
6
7type Job<State> = Box<dyn FnOnce(&mut State) + Send>;
8
9pub struct BoundedWorkers<State> {
12 inner: Arc<Workers<State>>,
13}
14
15impl<State> Clone for BoundedWorkers<State> {
16 fn clone(&self) -> Self {
17 Self {
18 inner: self.inner.clone(),
19 }
20 }
21}
22
23impl<State: Send + 'static> BoundedWorkers<State> {
24 pub fn start(states: Vec<State>, capacity: NonZeroUsize, name: &str) -> std::io::Result<Self> {
26 assert!(
27 !states.is_empty(),
28 "a worker pool needs at least one worker"
29 );
30 let (sender, receiver) = mpsc::channel::<Job<State>>(capacity.get());
31 let receiver = Arc::new(Mutex::new(receiver));
32 let mut joins = Vec::with_capacity(states.len());
33 for (index, mut state) in states.into_iter().enumerate() {
34 let receiver = receiver.clone();
35 joins.push(
36 std::thread::Builder::new()
37 .name(format!("{name}-{index}"))
38 .spawn(move || {
39 loop {
40 let job = receiver
43 .lock()
44 .expect("worker queue mutex poisoned")
45 .blocking_recv();
46 match job {
47 Some(job) => job(&mut state),
48 None => break,
49 }
50 }
51 })?,
52 );
53 }
54 Ok(Self {
55 inner: Arc::new(Workers {
56 sender: Some(sender),
57 joins,
58 }),
59 })
60 }
61
62 pub async fn call<F, R>(&self, operation: F) -> R
66 where
67 F: FnOnce(&mut State) -> R + Send + 'static,
68 R: Send + 'static,
69 {
70 let (reply, result) = oneshot::channel();
71 let job: Job<State> = Box::new(move |state| {
72 if reply.is_closed() {
73 tracing::debug!("discarding cancelled queued work");
74 return;
75 }
76 let outcome =
77 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| operation(state)));
78 if reply.send(outcome).is_err() {
80 tracing::debug!("discarding completed work for a cancelled caller");
81 }
82 });
83 if self
84 .inner
85 .sender
86 .as_ref()
87 .expect("live workers retain their sender")
88 .send(job)
89 .await
90 .is_err()
91 {
92 panic!("worker pool stopped before admitting a call");
93 }
94 match result.await {
95 Ok(Ok(value)) => value,
96 Ok(Err(panic)) => std::panic::resume_unwind(panic),
97 Err(_) => panic!("worker pool dropped a call without responding"),
98 }
99 }
100}
101
102struct Workers<State> {
103 sender: Option<mpsc::Sender<Job<State>>>,
104 joins: Vec<std::thread::JoinHandle<()>>,
105}
106
107impl<State> Drop for Workers<State> {
108 fn drop(&mut self) {
109 drop(self.sender.take());
112 let current_thread = std::thread::current().id();
113 let on_owned_worker = self
114 .joins
115 .iter()
116 .any(|join| join.thread().id() == current_thread);
117 if !on_owned_worker && tokio::runtime::Handle::try_current().is_err() {
118 for join in self.joins.drain(..) {
119 if join.join().is_err() {
120 tracing::error!("bounded worker panicked");
121 }
122 }
123 }
124 }
125}
126
127#[cfg(test)]
128#[path = "bounded_workers_tests.rs"]
129mod tests;