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 _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    /// Attempts to spawn a closure if this worker has no task running or queued.
84    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
113/// Decrements a worker's pending task count when a task finishes, including after panic.
114struct DecrementPendingOnDrop(Arc<AtomicUsize>);
115
116impl Drop for DecrementPendingOnDrop {
117    fn drop(&mut self) {
118        self.0.fetch_sub(1, Ordering::AcqRel);
119    }
120}
121
122/// Records a worker task's run time when the task finishes or unwinds.
123struct 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
140/// A map of named single-thread workers.
141///
142/// Each unique name gets a dedicated OS thread that is reused for all tasks submitted under
143/// that name. Workers are created lazily on first use.
144pub(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    /// Creates a new empty `WorkerMap`.
156    pub(crate) fn new() -> Self {
157        Self { workers: DashMap::new() }
158    }
159
160    /// Spawns a closure on the dedicated worker thread for the given name.
161    ///
162    /// If no worker thread exists for this name yet, one is created with the given name as
163    /// the OS thread name. The closure executes on the worker's OS thread and the returned
164    /// future resolves with the result.
165    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    /// Attempts to spawn a closure on the dedicated worker thread for the given name.
175    ///
176    /// Returns `None` if the named worker already has a task running or queued.
177    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 sender so the thread's recv loop exits, then join.
195            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}