1use crate::metrics::WorkerThreadMetrics;
8use dashmap::DashMap;
9use std::{
10 panic::AssertUnwindSafe,
11 sync::{
12 atomic::{AtomicUsize, Ordering},
13 Arc,
14 },
15 thread,
16 time::Instant,
17};
18use tokio::sync::{mpsc, oneshot};
19
20type BoxedTask = Box<dyn FnOnce() + Send + 'static>;
21
22struct WorkerThread {
24 tx: mpsc::UnboundedSender<BoxedTask>,
26 metrics: WorkerThreadMetrics,
28 pending: Arc<AtomicUsize>,
30 handle: Option<thread::JoinHandle<()>>,
32}
33
34impl WorkerThread {
35 fn new(name: &'static str) -> Self {
37 let (tx, mut rx) = mpsc::unbounded_channel::<BoxedTask>();
38 let handle = thread::Builder::new()
39 .name(name.to_string())
40 .spawn(move || {
41 while let Some(task) = rx.blocking_recv() {
42 let _ = std::panic::catch_unwind(AssertUnwindSafe(task));
43 }
44 })
45 .unwrap_or_else(|e| panic!("failed to spawn worker thread {name:?}: {e}"));
46
47 Self {
48 tx,
49 metrics: WorkerThreadMetrics::new(name),
50 pending: Arc::new(AtomicUsize::new(0)),
51 handle: Some(handle),
52 }
53 }
54
55 fn spawn<F, R>(&self, f: F) -> oneshot::Receiver<R>
57 where
58 F: FnOnce() -> R + Send + 'static,
59 R: Send + 'static,
60 {
61 self.pending.fetch_add(1, Ordering::AcqRel);
62
63 let (result_tx, result_rx) = oneshot::channel();
64 let pending = self.pending.clone();
65 let metrics = self.metrics.clone();
66 let queued_at = Instant::now();
67
68 let task: BoxedTask = Box::new(move || {
69 let started_at = Instant::now();
70 metrics.record_queue_wait(started_at.saturating_duration_since(queued_at));
71 let _decrement_pending = DecrementPendingOnDrop(pending);
72 let _record_task_duration = RecordTaskDurationOnDrop::new(metrics, started_at);
73 let _ = result_tx.send(f());
74 });
75
76 if self.tx.send(task).is_err() {
77 self.pending.fetch_sub(1, Ordering::AcqRel);
78 }
79
80 result_rx
81 }
82
83 fn try_spawn<F, R>(&self, f: F) -> Option<oneshot::Receiver<R>>
85 where
86 F: FnOnce() -> R + Send + 'static,
87 R: Send + 'static,
88 {
89 self.pending.compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire).ok()?;
90
91 let (result_tx, result_rx) = oneshot::channel();
92 let pending = self.pending.clone();
93 let metrics = self.metrics.clone();
94 let queued_at = Instant::now();
95
96 let task: BoxedTask = Box::new(move || {
97 let started_at = Instant::now();
98 metrics.record_queue_wait(started_at.saturating_duration_since(queued_at));
99 let _decrement_pending = DecrementPendingOnDrop(pending);
100 let _record_task_duration = RecordTaskDurationOnDrop::new(metrics, started_at);
101 let _ = result_tx.send(f());
102 });
103
104 if self.tx.send(task).is_err() {
105 self.pending.fetch_sub(1, Ordering::AcqRel);
106 return None
107 }
108
109 Some(result_rx)
110 }
111}
112
113struct DecrementPendingOnDrop(Arc<AtomicUsize>);
115
116impl Drop for DecrementPendingOnDrop {
117 fn drop(&mut self) {
118 self.0.fetch_sub(1, Ordering::AcqRel);
119 }
120}
121
122struct RecordTaskDurationOnDrop {
124 metrics: WorkerThreadMetrics,
125 started_at: Instant,
126}
127
128impl RecordTaskDurationOnDrop {
129 const fn new(metrics: WorkerThreadMetrics, started_at: Instant) -> Self {
130 Self { metrics, started_at }
131 }
132}
133
134impl Drop for RecordTaskDurationOnDrop {
135 fn drop(&mut self) {
136 self.metrics.record_task_duration(self.started_at.elapsed());
137 }
138}
139
140pub(crate) struct WorkerMap {
145 workers: DashMap<&'static str, WorkerThread>,
146}
147
148impl Default for WorkerMap {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154impl WorkerMap {
155 pub(crate) fn new() -> Self {
157 Self { workers: DashMap::new() }
158 }
159
160 pub(crate) fn spawn_on<F, R>(&self, name: &'static str, f: F) -> oneshot::Receiver<R>
166 where
167 F: FnOnce() -> R + Send + 'static,
168 R: Send + 'static,
169 {
170 let worker = self.workers.entry(name).or_insert_with(|| WorkerThread::new(name));
171 worker.spawn(f)
172 }
173
174 pub(crate) fn try_spawn_on<F, R>(
178 &self,
179 name: &'static str,
180 f: F,
181 ) -> Option<oneshot::Receiver<R>>
182 where
183 F: FnOnce() -> R + Send + 'static,
184 R: Send + 'static,
185 {
186 let worker = self.workers.entry(name).or_insert_with(|| WorkerThread::new(name));
187 worker.try_spawn(f)
188 }
189}
190
191impl Drop for WorkerMap {
192 fn drop(&mut self) {
193 for (_, mut w) in std::mem::take(&mut self.workers) {
194 drop(w.tx);
196 if let Some(handle) = w.handle.take() {
197 let _ = handle.join();
198 }
199 }
200 }
201}
202
203impl std::fmt::Debug for WorkerMap {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 f.debug_struct("WorkerMap").field("num_workers", &self.workers.len()).finish()
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[tokio::test]
214 async fn worker_map_basic() {
215 let map = WorkerMap::new();
216
217 let result = map.spawn_on("test", || 42).await.unwrap();
218 assert_eq!(result, 42);
219 }
220
221 #[tokio::test]
222 async fn worker_map_same_thread() {
223 let map = WorkerMap::new();
224
225 let id1 = map.spawn_on("test", || thread::current().id()).await.unwrap();
226 let id2 = map.spawn_on("test", || thread::current().id()).await.unwrap();
227 assert_eq!(id1, id2, "same name should run on the same thread");
228 }
229
230 #[tokio::test]
231 async fn worker_map_different_names_different_threads() {
232 let map = WorkerMap::new();
233
234 let id1 = map.spawn_on("worker-a", || thread::current().id()).await.unwrap();
235 let id2 = map.spawn_on("worker-b", || thread::current().id()).await.unwrap();
236 assert_ne!(id1, id2, "different names should run on different threads");
237 }
238
239 #[tokio::test]
240 async fn worker_map_sequential_execution() {
241 use std::sync::{
242 atomic::{AtomicUsize, Ordering},
243 Arc,
244 };
245
246 let map = WorkerMap::new();
247 let counter = Arc::new(AtomicUsize::new(0));
248
249 let mut receivers = Vec::new();
250 for i in 0..10 {
251 let c = counter.clone();
252 let rx = map.spawn_on("sequential", move || {
253 let val = c.fetch_add(1, Ordering::SeqCst);
254 assert_eq!(val, i, "tasks should execute in order");
255 val
256 });
257 receivers.push(rx);
258 }
259
260 for (i, rx) in receivers.into_iter().enumerate() {
261 let val = rx.await.unwrap();
262 assert_eq!(val, i);
263 }
264 }
265
266 #[tokio::test]
267 async fn worker_map_thread_name() {
268 let map = WorkerMap::new();
269
270 let name = map
271 .spawn_on("custom-worker", || thread::current().name().unwrap().to_string())
272 .await
273 .unwrap();
274 assert_eq!(name, "custom-worker");
275 }
276
277 #[tokio::test]
278 async fn worker_map_try_spawn_busy() {
279 let map = WorkerMap::new();
280 let (release_tx, release_rx) = std::sync::mpsc::channel();
281
282 let first = map.try_spawn_on("busy-worker", move || {
283 release_rx.recv().unwrap();
284 1
285 });
286 assert!(first.is_some());
287
288 let second = map.try_spawn_on("busy-worker", || 2);
289 assert!(second.is_none(), "busy worker should reject queued work");
290
291 release_tx.send(()).unwrap();
292 assert_eq!(first.unwrap().await.unwrap(), 1);
293
294 let third = map.try_spawn_on("busy-worker", || 3).expect("worker should be idle");
295 assert_eq!(third.await.unwrap(), 3);
296 }
297}