Skip to main content

reth_tasks/
worker_map.rs

1//! A map of named single-thread worker pools.
2//!
3//! Each worker is a dedicated OS thread that processes closures sent to it via a channel.
4//! This is a substitute for `spawn_blocking` that reuses the same OS thread for the same
5//! named task, like a 1-thread thread pool keyed by name.
6
7use 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
22/// A single-thread worker that processes closures sequentially on a dedicated OS thread.
23struct WorkerThread {
24    /// Sender to submit work to this worker's thread.
25    tx: mpsc::UnboundedSender<BoxedTask>,
26    /// Metrics labeled with this worker's name.
27    metrics: WorkerThreadMetrics,
28    /// Number of tasks currently running or queued on this worker.
29    pending: Arc<AtomicUsize>,
30    /// The OS thread handle. Taken during shutdown to join.
31    handle: Option<thread::JoinHandle<()>>,
32}
33
34impl WorkerThread {
35    /// Spawns a new worker thread with the given name.
36    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    /// Spawns a closure on this worker.
56    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    /// Attempts to spawn a closure if this worker has no task running or queued.
87    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
119/// Decrements a worker's pending task count when a task finishes, including after panic.
120struct DecrementPendingOnDrop(Arc<AtomicUsize>);
121
122impl Drop for DecrementPendingOnDrop {
123    fn drop(&mut self) {
124        self.0.fetch_sub(1, Ordering::AcqRel);
125    }
126}
127
128/// Records a worker task's run time when the task finishes or unwinds.
129struct 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
146/// A map of named single-thread workers.
147///
148/// Each unique name gets a dedicated OS thread that is reused for all tasks submitted under
149/// that name. Workers are created lazily on first use.
150pub(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    /// Creates a new empty `WorkerMap`.
162    pub(crate) fn new() -> Self {
163        Self { workers: DashMap::new() }
164    }
165
166    /// Spawns a closure on the dedicated worker thread for the given name.
167    ///
168    /// If no worker thread exists for this name yet, one is created with the given name as
169    /// the OS thread name. The closure executes on the worker's OS thread and the returned
170    /// future resolves with the result.
171    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    /// Attempts to spawn a closure on the dedicated worker thread for the given name.
181    ///
182    /// Returns `None` if the named worker already has a task running or queued.
183    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 sender so the thread's recv loop exits, then join.
201            drop(w.tx);
202            // A task can own the last reference to this map and drop it on its own worker, which
203            // cannot join itself. That thread exits on its own once the task returns.
204            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            // Drops the last reference to the map on the worker it would join.
317            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}