qubit_thread_pool/fixed/fixed_thread_pool.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use std::{
11 sync::Arc,
12 thread::JoinHandle,
13};
14
15use qubit_executor::service::{
16 ExecutorService,
17 ExecutorServiceLifecycle,
18 StopReport,
19 SubmissionError,
20};
21use qubit_executor::task::spi::TaskEndpointPair;
22use qubit_executor::{
23 TaskHandle,
24 TrackedTask,
25};
26use qubit_function::{
27 Callable,
28 Runnable,
29};
30
31use super::fixed_thread_pool_builder::FixedThreadPoolBuilder;
32use super::fixed_thread_pool_inner::FixedThreadPoolInner;
33use super::fixed_worker::FixedWorker;
34use super::fixed_worker_runtime::FixedWorkerRuntime;
35use crate::{
36 ExecutorServiceBuilderError,
37 PoolJob,
38 ThreadPoolStats,
39};
40
41/// Fixed-size thread pool implementing [`ExecutorService`].
42///
43/// `FixedThreadPool` prestarts a fixed number of worker threads and does not
44/// support runtime pool-size changes. Use [`crate::ThreadPool`] when dynamic
45/// core/maximum sizes or keep-alive policies are required.
46pub struct FixedThreadPool {
47 /// Shared fixed pool state.
48 inner: Arc<FixedThreadPoolInner>,
49}
50
51impl FixedThreadPool {
52 /// Builds a fixed pool from a validated [`FixedThreadPoolBuilder`].
53 ///
54 /// # Parameters
55 ///
56 /// * `builder` - Configuration produced by [`FixedThreadPoolBuilder`].
57 ///
58 /// # Returns
59 ///
60 /// A fixed thread-pool handle with workers already started.
61 ///
62 /// # Errors
63 ///
64 /// Returns [`ExecutorServiceBuilderError`] when a worker thread cannot be spawned.
65 pub(crate) fn new_with_builder(builder: FixedThreadPoolBuilder) -> Result<Self, ExecutorServiceBuilderError> {
66 let FixedThreadPoolBuilder {
67 pool_size,
68 queue_capacity,
69 thread_name_prefix,
70 stack_size,
71 hooks,
72 } = builder;
73 let mut worker_runtimes = Vec::with_capacity(pool_size);
74 for index in 0..pool_size {
75 let worker_runtime = FixedWorkerRuntime::new(index);
76 worker_runtimes.push(worker_runtime);
77 }
78 let inner = Arc::new(FixedThreadPoolInner::with_hooks(pool_size, queue_capacity, hooks));
79 let mut worker_handles = Vec::with_capacity(pool_size);
80 for (index, worker_runtime) in worker_runtimes.into_iter().enumerate() {
81 inner.reserve_worker_slot();
82 let worker_inner = Arc::clone(&inner);
83 let thread_name = format!("{}-{}", thread_name_prefix, index);
84 let mut builder = std::thread::Builder::new().name(thread_name);
85 if let Some(stack_size) = stack_size {
86 builder = builder.stack_size(stack_size);
87 }
88 match builder.spawn(move || FixedWorker::run(worker_inner, worker_runtime)) {
89 Ok(handle) => worker_handles.push(handle),
90 Err(source) => {
91 inner.rollback_worker_slot();
92 inner.stop_after_failed_build();
93 join_started_workers(worker_handles);
94 return Err(ExecutorServiceBuilderError::SpawnWorker {
95 index: Some(index),
96 source,
97 });
98 }
99 }
100 }
101 Ok(Self { inner })
102 }
103
104 /// Creates a fixed thread pool with `pool_size` prestarted workers.
105 ///
106 /// # Parameters
107 ///
108 /// * `pool_size` - Number of worker threads.
109 ///
110 /// # Returns
111 ///
112 /// A fixed thread pool.
113 ///
114 /// # Errors
115 ///
116 /// Returns [`ExecutorServiceBuilderError`] if the worker count is zero or a worker
117 /// cannot be spawned.
118 pub fn new(pool_size: usize) -> Result<Self, ExecutorServiceBuilderError> {
119 Self::builder().pool_size(pool_size).build()
120 }
121
122 /// Creates a fixed pool builder.
123 ///
124 /// # Returns
125 ///
126 /// Builder with CPU parallelism defaults.
127 pub fn builder() -> FixedThreadPoolBuilder {
128 FixedThreadPoolBuilder::new()
129 }
130
131 /// Returns the fixed worker count.
132 ///
133 /// # Returns
134 ///
135 /// Number of workers in this pool.
136 pub fn pool_size(&self) -> usize {
137 self.inner.pool_size()
138 }
139
140 /// Returns the queued task count.
141 ///
142 /// # Returns
143 ///
144 /// Number of accepted tasks waiting to run.
145 pub fn queued_count(&self) -> usize {
146 self.inner.queued_count()
147 }
148
149 /// Returns the running task count.
150 ///
151 /// # Returns
152 ///
153 /// Number of tasks currently held by workers.
154 pub fn running_count(&self) -> usize {
155 self.inner.running_count()
156 }
157
158 /// Returns the live worker count.
159 ///
160 /// # Returns
161 ///
162 /// Number of worker loops that have not exited.
163 pub fn live_worker_count(&self) -> usize {
164 self.inner.state.read(|state| state.live_workers)
165 }
166
167 /// Returns a point-in-time stats snapshot.
168 ///
169 /// # Returns
170 ///
171 /// Snapshot containing queue, worker, and lifecycle counters.
172 pub fn stats(&self) -> ThreadPoolStats {
173 self.inner.stats()
174 }
175
176 /// Blocks until all accepted work has completed.
177 ///
178 /// This is a join-style wait for quiescence: it does not request shutdown
179 /// and does not wait for worker threads to exit. Concurrent submissions may
180 /// extend the wait until those accepted jobs also drain.
181 #[inline]
182 pub fn join(&self) {
183 self.inner.wait_until_idle();
184 }
185}
186
187impl Default for FixedThreadPool {
188 /// Creates a fixed thread pool using [`FixedThreadPoolBuilder::default`].
189 ///
190 /// # Returns
191 ///
192 /// A fixed thread pool with CPU parallelism defaults and prestarted workers.
193 ///
194 /// # Panics
195 ///
196 /// Panics when the default builder fails to spawn a worker thread.
197 fn default() -> Self {
198 FixedThreadPoolBuilder::default()
199 .build()
200 .expect("failed to build default FixedThreadPool")
201 }
202}
203
204impl Drop for FixedThreadPool {
205 /// Requests graceful shutdown when the pool handle is dropped.
206 fn drop(&mut self) {
207 self.inner.shutdown();
208 }
209}
210
211impl ExecutorService for FixedThreadPool {
212 type ResultHandle<R, E>
213 = TaskHandle<R, E>
214 where
215 R: Send + 'static,
216 E: Send + 'static;
217
218 type TrackedHandle<R, E>
219 = TrackedTask<R, E>
220 where
221 R: Send + 'static,
222 E: Send + 'static;
223
224 /// Accepts a runnable and queues it for fixed pool workers.
225 fn submit<T, E>(&self, task: T) -> Result<(), SubmissionError>
226 where
227 T: Runnable<E> + Send + 'static,
228 E: Send + 'static,
229 {
230 self.inner.submit(PoolJob::detached(task))
231 }
232
233 /// Accepts a callable and queues it for fixed pool workers.
234 ///
235 /// # Parameters
236 ///
237 /// * `task` - Callable to execute on a fixed pool worker.
238 ///
239 /// # Returns
240 ///
241 /// A [`TaskHandle`] for the accepted task.
242 ///
243 /// # Errors
244 ///
245 /// Returns [`SubmissionError::Shutdown`] after shutdown or
246 /// [`SubmissionError::Saturated`] when a bounded queue is full.
247 fn submit_callable<C, R, E>(&self, task: C) -> Result<Self::ResultHandle<R, E>, SubmissionError>
248 where
249 C: Callable<R, E> + Send + 'static,
250 R: Send + 'static,
251 E: Send + 'static,
252 {
253 let (handle, completion) = TaskEndpointPair::new().into_parts();
254 let job = PoolJob::from_task(task, completion);
255 self.inner.submit(job)?;
256 Ok(handle)
257 }
258
259 /// Accepts a callable and queues it with a tracked handle.
260 ///
261 /// # Parameters
262 ///
263 /// * `task` - Callable to execute on a fixed pool worker.
264 ///
265 /// # Returns
266 ///
267 /// A [`TrackedTask`] that reports task status and can observe completion,
268 /// failure, or queued cancellation.
269 ///
270 /// # Errors
271 ///
272 /// Returns [`SubmissionError::Shutdown`] after shutdown or
273 /// [`SubmissionError::Saturated`] when a bounded queue is full.
274 fn submit_tracked_callable<C, R, E>(&self, task: C) -> Result<Self::TrackedHandle<R, E>, SubmissionError>
275 where
276 C: Callable<R, E> + Send + 'static,
277 R: Send + 'static,
278 E: Send + 'static,
279 {
280 let (handle, completion) = TaskEndpointPair::new().into_tracked_parts();
281 let job = PoolJob::from_task(task, completion);
282 self.inner.submit(job)?;
283 Ok(handle)
284 }
285
286 /// Stops accepting new work and drains accepted queued tasks.
287 fn shutdown(&self) {
288 self.inner.shutdown();
289 }
290
291 /// Stops accepting work and cancels queued tasks.
292 ///
293 /// # Returns
294 ///
295 /// A count-based shutdown report.
296 fn stop(&self) -> StopReport {
297 self.inner.stop()
298 }
299
300 /// Returns the current lifecycle state.
301 fn lifecycle(&self) -> ExecutorServiceLifecycle {
302 self.inner.lifecycle()
303 }
304
305 /// Returns whether shutdown has been requested.
306 ///
307 /// # Returns
308 ///
309 /// `true` when this pool no longer accepts new work.
310 fn is_not_running(&self) -> bool {
311 self.inner.is_not_running()
312 }
313
314 /// Returns whether this pool is fully terminated.
315 ///
316 /// # Returns
317 ///
318 /// `true` after shutdown and after all workers have exited.
319 fn is_terminated(&self) -> bool {
320 self.inner.is_terminated()
321 }
322
323 /// Blocks until this fixed pool has terminated.
324 fn wait_termination(&self) {
325 self.inner.wait_for_termination();
326 }
327}
328
329/// Joins workers that were already spawned during a failed build.
330///
331/// # Parameters
332///
333/// * `worker_handles` - Join handles for workers started before construction failed.
334fn join_started_workers(worker_handles: Vec<JoinHandle<()>>) {
335 for worker_handle in worker_handles {
336 let _ignored = worker_handle.join();
337 }
338}