cu29_runtime/
thread_pool.rs1#[cfg(feature = "rt-scheduling")]
22use crate::config::OnError;
23use crate::config::{SchedulingPolicy, ThreadPoolConfig};
24#[allow(unused_imports)] use crate::log::*;
26use cu29_traits::{CuError, CuResult};
27use rayon::ThreadPool;
28
29pub fn build_pool(spec: &ThreadPoolConfig) -> CuResult<ThreadPool> {
32 let id = spec.id.clone();
33 let pool = rayon::ThreadPoolBuilder::new()
34 .num_threads(spec.threads)
35 .thread_name({
36 let id = id.clone();
37 move |i| format!("cu-pool-{id}-{i}")
38 })
39 .build()
40 .map_err(|e| CuError::from(format!("Failed to build thread pool '{id}': {e}")))?;
41
42 apply_scheduling(&pool, spec)?;
43 Ok(pool)
44}
45
46#[cfg(feature = "rt-scheduling")]
47fn apply_scheduling(pool: &ThreadPool, spec: &ThreadPoolConfig) -> CuResult<()> {
48 if spec.affinity.is_none() && spec.policy == SchedulingPolicy::Fair {
50 return Ok(());
51 }
52
53 let affinity = spec.affinity.as_deref();
54 let policy = spec.policy;
55
56 let results: Vec<CuResult<()>> =
60 pool.broadcast(|ctx| apply_to_current_thread(affinity, policy, ctx.index()));
61
62 for (worker, result) in results.into_iter().enumerate() {
63 if let Err(e) = result {
64 match spec.on_error {
65 OnError::Strict => {
66 return Err(CuError::from(format!(
67 "Thread pool '{}' worker {worker} could not apply scheduling: {e}",
68 spec.id
69 )));
70 }
71 OnError::Warn => {
72 let pool_id = spec.id.as_str();
73 let reason = e.to_string();
74 warning!(
75 "Thread pool {} worker {} could not apply scheduling ({}); using default scheduling",
76 pool_id,
77 worker,
78 reason.as_str()
79 );
80 }
81 }
82 }
83 }
84
85 Ok(())
86}
87
88#[cfg(not(feature = "rt-scheduling"))]
89fn apply_scheduling(_pool: &ThreadPool, spec: &ThreadPoolConfig) -> CuResult<()> {
90 if spec.affinity.is_some() || spec.policy != SchedulingPolicy::Fair {
91 let pool_id = spec.id.as_str();
92 warning!(
93 "Thread pool {} requests CPU affinity/scheduling but the 'rt-scheduling' feature is disabled; using default scheduling",
94 pool_id
95 );
96 }
97 Ok(())
98}
99
100#[cfg(feature = "rt-scheduling")]
110pub fn apply_current_thread_scheduling(spec: &ThreadPoolConfig, index: usize) -> CuResult<()> {
111 if spec.affinity.is_none() && spec.policy == SchedulingPolicy::Fair {
112 return Ok(());
113 }
114 match apply_to_current_thread(spec.affinity.as_deref(), spec.policy, index) {
115 Ok(()) => Ok(()),
116 Err(e) => {
117 let pool_id = spec.id.as_str();
118 let reason = e.to_string();
119 match spec.on_error {
120 OnError::Strict => {
121 error!(
122 "Thread pool {} worker {} could not apply scheduling: {}",
123 pool_id,
124 index,
125 reason.as_str()
126 );
127 Err(CuError::from(format!(
128 "Thread pool '{}' worker {index} could not apply scheduling: {reason}",
129 spec.id
130 )))
131 }
132 OnError::Warn => {
133 warning!(
134 "Thread pool {} worker {} could not apply scheduling ({}); using default scheduling",
135 pool_id,
136 index,
137 reason.as_str()
138 );
139 Ok(())
140 }
141 }
142 }
143 }
144}
145
146#[cfg(not(feature = "rt-scheduling"))]
147pub fn apply_current_thread_scheduling(spec: &ThreadPoolConfig, _index: usize) -> CuResult<()> {
148 if spec.affinity.is_some() || spec.policy != SchedulingPolicy::Fair {
149 let pool_id = spec.id.as_str();
150 warning!(
151 "Thread pool {} requests CPU affinity/scheduling but the 'rt-scheduling' feature is disabled; using default scheduling",
152 pool_id
153 );
154 }
155 Ok(())
156}
157
158#[cfg(feature = "rt-scheduling")]
159fn apply_to_current_thread(
160 affinity: Option<&[usize]>,
161 policy: SchedulingPolicy,
162 index: usize,
163) -> CuResult<()> {
164 if let Some(cores) = affinity
165 && !cores.is_empty()
166 {
167 let core = cores[index % cores.len()];
168 set_affinity(core)?;
169 }
170 set_policy(policy)
171}
172
173#[cfg(feature = "rt-scheduling")]
174fn set_affinity(core: usize) -> CuResult<()> {
175 if core_affinity::set_for_current(core_affinity::CoreId { id: core }) {
176 Ok(())
177 } else {
178 Err(CuError::from(format!(
179 "failed to pin worker to CPU core {core}"
180 )))
181 }
182}
183
184#[cfg(all(feature = "rt-scheduling", target_os = "linux"))]
185fn set_policy(policy: SchedulingPolicy) -> CuResult<()> {
186 match policy {
187 SchedulingPolicy::Fair => Ok(()),
188 SchedulingPolicy::Nice(nice) => set_nice(nice),
189 SchedulingPolicy::Fifo { priority } => set_rt_policy(libc::SCHED_FIFO, priority),
190 SchedulingPolicy::RoundRobin { priority } => set_rt_policy(libc::SCHED_RR, priority),
191 }
192}
193
194#[cfg(all(feature = "rt-scheduling", not(target_os = "linux")))]
195fn set_policy(policy: SchedulingPolicy) -> CuResult<()> {
196 match policy {
197 SchedulingPolicy::Fair => Ok(()),
198 _ => Err(CuError::from(
199 "real-time scheduling policies are only supported on Linux",
200 )),
201 }
202}
203
204#[cfg(all(feature = "rt-scheduling", target_os = "linux"))]
205fn set_rt_policy(policy: libc::c_int, priority: u8) -> CuResult<()> {
206 let param = libc::sched_param {
207 sched_priority: libc::c_int::from(priority),
208 };
209 let ret = unsafe { libc::pthread_setschedparam(libc::pthread_self(), policy, ¶m) };
212 if ret != 0 {
213 return Err(CuError::from(format!(
215 "pthread_setschedparam failed ({}); setting a real-time priority typically requires CAP_SYS_NICE",
216 std::io::Error::from_raw_os_error(ret)
217 )));
218 }
219 Ok(())
220}
221
222#[cfg(all(feature = "rt-scheduling", target_os = "linux"))]
223fn set_nice(nice: i8) -> CuResult<()> {
224 let tid = unsafe { libc::syscall(libc::SYS_gettid) } as libc::id_t;
227
228 unsafe { *libc::__errno_location() = 0 };
232 let ret = unsafe { libc::setpriority(libc::PRIO_PROCESS, tid, libc::c_int::from(nice)) };
234 if ret == -1 {
235 let err = std::io::Error::last_os_error();
236 if err.raw_os_error().unwrap_or(0) != 0 {
237 return Err(CuError::from(format!("setpriority failed: {err}")));
238 }
239 }
240 Ok(())
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::config::{OnError, SchedulingPolicy, ThreadPoolConfig};
247
248 fn spec(id: &str, threads: usize) -> ThreadPoolConfig {
249 ThreadPoolConfig {
250 id: id.to_string(),
251 threads,
252 affinity: None,
253 policy: SchedulingPolicy::Fair,
254 on_error: OnError::Warn,
255 }
256 }
257
258 #[test]
259 fn builds_plain_pool_with_requested_thread_count() {
260 let pool = build_pool(&spec("plain", 3)).unwrap();
261 assert_eq!(pool.current_num_threads(), 3);
262 }
263
264 #[test]
265 fn warn_mode_tolerates_unappliable_request() {
266 let mut s = spec("warn", 2);
269 s.policy = SchedulingPolicy::Fifo { priority: 50 };
270 let pool = build_pool(&s).unwrap();
271 assert_eq!(pool.current_num_threads(), 2);
272 }
273
274 #[cfg(all(feature = "rt-scheduling", target_os = "linux"))]
279 #[test]
280 fn strict_mode_fails_on_invalid_affinity() {
281 let Some(cores) = core_affinity::get_core_ids() else {
282 return; };
284 let max_core = cores.iter().map(|c| c.id).max().unwrap_or(0);
285 let invalid_core = max_core.saturating_add(1);
286 let mut s = spec("strict", 1);
287 s.affinity = Some(vec![invalid_core]);
288 s.on_error = OnError::Strict;
289 assert!(build_pool(&s).is_err());
290 }
291
292 #[cfg(feature = "rt-scheduling")]
294 #[test]
295 fn affinity_to_valid_cores_succeeds() {
296 let Some(cores) = core_affinity::get_core_ids() else {
297 return; };
299 if cores.is_empty() {
300 return;
301 }
302 let mut s = spec("affinity", cores.len());
303 s.affinity = Some(cores.iter().map(|c| c.id).collect());
304 s.on_error = OnError::Strict;
305 let pool = build_pool(&s).unwrap();
306 assert_eq!(pool.current_num_threads(), cores.len());
307 }
308}