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 result = {
72 let _decrement_pending = DecrementPendingOnDrop(pending);
73 let _record_task_duration = RecordTaskDurationOnDrop::new(metrics, started_at);
74 f()
75 };
76 let _ = result_tx.send(result);
77 });
78
79 if self.tx.send(task).is_err() {
80 self.pending.fetch_sub(1, Ordering::AcqRel);
81 }
82
83 result_rx
84 }
85
86 fn try_spawn<F, R>(&self, f: F) -> Option<oneshot::Receiver<R>>
88 where
89 F: FnOnce() -> R + Send + 'static,
90 R: Send + 'static,
91 {
92 self.pending.compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire).ok()?;
93
94 let (result_tx, result_rx) = oneshot::channel();
95 let pending = self.pending.clone();
96 let metrics = self.metrics.clone();
97 let queued_at = Instant::now();
98
99 let task: BoxedTask = Box::new(move || {
100 let started_at = Instant::now();
101 metrics.record_queue_wait(started_at.saturating_duration_since(queued_at));
102 let result = {
103 let _decrement_pending = DecrementPendingOnDrop(pending);
104 let _record_task_duration = RecordTaskDurationOnDrop::new(metrics, started_at);
105 f()
106 };
107 let _ = result_tx.send(result);
108 });
109
110 if self.tx.send(task).is_err() {
111 self.pending.fetch_sub(1, Ordering::AcqRel);
112 return None
113 }
114
115 Some(result_rx)
116 }
117}
118
119struct DecrementPendingOnDrop(Arc<AtomicUsize>);
121
122impl Drop for DecrementPendingOnDrop {
123 fn drop(&mut self) {
124 self.0.fetch_sub(1, Ordering::AcqRel);
125 }
126}
127
128struct RecordTaskDurationOnDrop {
130 metrics: WorkerThreadMetrics,
131 started_at: Instant,
132}
133
134impl RecordTaskDurationOnDrop {
135 const fn new(metrics: WorkerThreadMetrics, started_at: Instant) -> Self {
136 Self { metrics, started_at }
137 }
138}
139
140impl Drop for RecordTaskDurationOnDrop {
141 fn drop(&mut self) {
142 self.metrics.record_task_duration(self.started_at.elapsed());
143 }
144}
145
146pub(crate) struct WorkerMap {
151 workers: DashMap<&'static str, WorkerThread>,
152}
153
154impl Default for WorkerMap {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160impl WorkerMap {
161 pub(crate) fn new() -> Self {
163 Self { workers: DashMap::new() }
164 }
165
166 pub(crate) fn spawn_on<F, R>(&self, name: &'static str, f: F) -> oneshot::Receiver<R>
172 where
173 F: FnOnce() -> R + Send + 'static,
174 R: Send + 'static,
175 {
176 let worker = self.workers.entry(name).or_insert_with(|| WorkerThread::new(name));
177 worker.spawn(f)
178 }
179
180 pub(crate) fn try_spawn_on<F, R>(
184 &self,
185 name: &'static str,
186 f: F,
187 ) -> Option<oneshot::Receiver<R>>
188 where
189 F: FnOnce() -> R + Send + 'static,
190 R: Send + 'static,
191 {
192 let worker = self.workers.entry(name).or_insert_with(|| WorkerThread::new(name));
193 worker.try_spawn(f)
194 }
195}
196
197impl Drop for WorkerMap {
198 fn drop(&mut self) {
199 for (_, mut w) in std::mem::take(&mut self.workers) {
200 drop(w.tx);
202 if let Some(handle) = w.handle.take() &&
205 handle.thread().id() != thread::current().id()
206 {
207 let _ = handle.join();
208 }
209 }
210 }
211}
212
213impl std::fmt::Debug for WorkerMap {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 f.debug_struct("WorkerMap").field("num_workers", &self.workers.len()).finish()
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[tokio::test]
224 async fn worker_map_basic() {
225 let map = WorkerMap::new();
226
227 let result = map.spawn_on("test", || 42).await.unwrap();
228 assert_eq!(result, 42);
229 }
230
231 #[tokio::test]
232 async fn worker_map_same_thread() {
233 let map = WorkerMap::new();
234
235 let id1 = map.spawn_on("test", || thread::current().id()).await.unwrap();
236 let id2 = map.spawn_on("test", || thread::current().id()).await.unwrap();
237 assert_eq!(id1, id2, "same name should run on the same thread");
238 }
239
240 #[tokio::test]
241 async fn worker_map_different_names_different_threads() {
242 let map = WorkerMap::new();
243
244 let id1 = map.spawn_on("worker-a", || thread::current().id()).await.unwrap();
245 let id2 = map.spawn_on("worker-b", || thread::current().id()).await.unwrap();
246 assert_ne!(id1, id2, "different names should run on different threads");
247 }
248
249 #[tokio::test]
250 async fn worker_map_sequential_execution() {
251 use std::sync::{
252 atomic::{AtomicUsize, Ordering},
253 Arc,
254 };
255
256 let map = WorkerMap::new();
257 let counter = Arc::new(AtomicUsize::new(0));
258
259 let mut receivers = Vec::new();
260 for i in 0..10 {
261 let c = counter.clone();
262 let rx = map.spawn_on("sequential", move || {
263 let val = c.fetch_add(1, Ordering::SeqCst);
264 assert_eq!(val, i, "tasks should execute in order");
265 val
266 });
267 receivers.push(rx);
268 }
269
270 for (i, rx) in receivers.into_iter().enumerate() {
271 let val = rx.await.unwrap();
272 assert_eq!(val, i);
273 }
274 }
275
276 #[tokio::test]
277 async fn worker_map_thread_name() {
278 let map = WorkerMap::new();
279
280 let name = map
281 .spawn_on("custom-worker", || thread::current().name().unwrap().to_string())
282 .await
283 .unwrap();
284 assert_eq!(name, "custom-worker");
285 }
286
287 #[tokio::test]
288 async fn worker_map_try_spawn_busy() {
289 let map = WorkerMap::new();
290 let (release_tx, release_rx) = std::sync::mpsc::channel();
291
292 let first = map.try_spawn_on("busy-worker", move || {
293 release_rx.recv().unwrap();
294 1
295 });
296 assert!(first.is_some());
297
298 let second = map.try_spawn_on("busy-worker", || 2);
299 assert!(second.is_none(), "busy worker should reject queued work");
300
301 release_tx.send(()).unwrap();
302 assert_eq!(first.unwrap().await.unwrap(), 1);
303
304 let third = map.try_spawn_on("busy-worker", || 3).expect("worker should be idle");
305 assert_eq!(third.await.unwrap(), 3);
306 }
307
308 #[test]
309 fn worker_map_last_owner_dropped_on_worker() {
310 let map = Arc::new(WorkerMap::new());
311 let owner = Arc::clone(&map);
312 let (release_tx, release_rx) = std::sync::mpsc::channel();
313
314 let done = map.spawn_on("last-owner", move || {
315 release_rx.recv().unwrap();
316 drop(owner);
318 });
319
320 drop(map);
321 release_tx.send(()).unwrap();
322 done.blocking_recv().expect("dropping the map on its own worker should not panic");
323 }
324}