weavatrix_scan/
runtime.rs1#![allow(clippy::missing_const_for_thread_local)]
2
3use crate::pool::{Job, ThreadPool};
4use std::cell::RefCell;
5use std::fmt;
6use std::io;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::{Arc, OnceLock};
9use std::time::Duration;
10
11thread_local! {
12 static ACTIVE_RUNTIMES: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
13}
14
15pub type ParallelJob = Box<dyn FnOnce() + Send + 'static>;
17
18pub trait ParallelExecutor: Send + Sync + 'static {
24 fn parallelism(&self) -> usize;
26
27 fn try_execute(&self, job: ParallelJob, busy_timeout: Option<Duration>) -> io::Result<()>;
34}
35
36enum Executor {
37 Global,
38 Dedicated(Arc<ThreadPool>),
39 External(Arc<dyn ParallelExecutor>),
40}
41
42struct RuntimeInner {
43 id: u64,
44 executor: Executor,
45 busy_timeout: Option<Duration>,
46}
47
48#[derive(Clone)]
54pub struct ParallelRuntime {
55 inner: Arc<RuntimeInner>,
56}
57
58impl ParallelRuntime {
59 #[must_use]
61 pub fn global() -> Self {
62 static RUNTIME: OnceLock<ParallelRuntime> = OnceLock::new();
63 RUNTIME
64 .get_or_init(|| Self::new(Executor::Global, None))
65 .clone()
66 }
67
68 pub fn dedicated(parallelism: usize) -> io::Result<Self> {
74 let pool = ThreadPool::with_workers(parallelism.max(1))?;
75 Ok(Self::new(Executor::Dedicated(Arc::new(pool)), None))
76 }
77
78 #[must_use]
80 pub fn external(executor: Arc<dyn ParallelExecutor>) -> Self {
81 Self::new(Executor::External(executor), None)
82 }
83
84 #[must_use]
86 pub fn with_busy_timeout(mut self, busy_timeout: Option<Duration>) -> Self {
87 Arc::make_mut(&mut self.inner).busy_timeout = busy_timeout;
88 self
89 }
90
91 #[must_use]
93 pub fn parallelism(&self) -> usize {
94 match &self.inner.executor {
95 Executor::Global => ThreadPool::global().workers(),
96 Executor::Dedicated(pool) => pool.workers(),
97 Executor::External(executor) => executor.parallelism().max(1),
98 }
99 }
100
101 pub(crate) fn is_worker_thread(&self) -> bool {
102 ACTIVE_RUNTIMES.with(|active| active.borrow().contains(&self.inner.id))
103 }
104
105 pub(crate) fn try_execute<F>(&self, job: F) -> io::Result<()>
106 where
107 F: FnOnce() + Send + 'static,
108 {
109 let id = self.inner.id;
110 let wrapped: Job = Box::new(move || {
111 let _guard = ActiveRuntimeGuard::enter(id);
112 job();
113 });
114 match &self.inner.executor {
115 Executor::Global => ThreadPool::global().execute(wrapped),
116 Executor::Dedicated(pool) => pool.execute(wrapped),
117 Executor::External(executor) => executor.try_execute(wrapped, self.inner.busy_timeout),
118 }
119 }
120
121 fn new(executor: Executor, busy_timeout: Option<Duration>) -> Self {
122 static NEXT_RUNTIME_ID: AtomicU64 = AtomicU64::new(1);
123 Self {
124 inner: Arc::new(RuntimeInner {
125 id: NEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed),
126 executor,
127 busy_timeout,
128 }),
129 }
130 }
131}
132
133impl Default for ParallelRuntime {
134 fn default() -> Self {
135 Self::global()
136 }
137}
138
139impl fmt::Debug for ParallelRuntime {
140 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141 let kind = match &self.inner.executor {
142 Executor::Global => "global",
143 Executor::Dedicated(_) => "dedicated",
144 Executor::External(_) => "external",
145 };
146 formatter
147 .debug_struct("ParallelRuntime")
148 .field("kind", &kind)
149 .field("parallelism", &self.parallelism())
150 .field("busy_timeout", &self.inner.busy_timeout)
151 .finish()
152 }
153}
154
155impl Clone for RuntimeInner {
156 fn clone(&self) -> Self {
157 Self {
158 id: self.id,
159 executor: match &self.executor {
160 Executor::Global => Executor::Global,
161 Executor::Dedicated(pool) => Executor::Dedicated(Arc::clone(pool)),
162 Executor::External(executor) => Executor::External(Arc::clone(executor)),
163 },
164 busy_timeout: self.busy_timeout,
165 }
166 }
167}
168
169struct ActiveRuntimeGuard;
170
171impl ActiveRuntimeGuard {
172 fn enter(id: u64) -> Self {
173 ACTIVE_RUNTIMES.with(|active| active.borrow_mut().push(id));
174 Self
175 }
176}
177
178impl Drop for ActiveRuntimeGuard {
179 fn drop(&mut self) {
180 ACTIVE_RUNTIMES.with(|active| {
181 active.borrow_mut().pop();
182 });
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::{ParallelExecutor, ParallelJob, ParallelRuntime};
189 use std::io;
190 use std::sync::{Arc, mpsc};
191 use std::time::Duration;
192
193 struct Inline;
194
195 impl ParallelExecutor for Inline {
196 fn parallelism(&self) -> usize {
197 1
198 }
199
200 fn try_execute(&self, job: ParallelJob, _busy_timeout: Option<Duration>) -> io::Result<()> {
201 job();
202 Ok(())
203 }
204 }
205
206 #[test]
207 fn dedicated_runtime_executes_and_joins() {
208 let runtime = ParallelRuntime::dedicated(2).unwrap();
209 let (sender, receiver) = mpsc::channel();
210 runtime
211 .try_execute(move || sender.send(9).unwrap())
212 .unwrap();
213 assert_eq!(receiver.recv().unwrap(), 9);
214 }
215
216 #[test]
217 fn external_runtime_marks_nested_execution() {
218 let runtime = ParallelRuntime::external(Arc::new(Inline));
219 let nested = runtime.clone();
220 let (sender, receiver) = mpsc::channel();
221 runtime
222 .try_execute(move || sender.send(nested.is_worker_thread()).unwrap())
223 .unwrap();
224 assert!(receiver.recv().unwrap());
225 }
226}