1use std::fmt;
2use std::sync::atomic::{AtomicBool, Ordering};
3
4#[repr(transparent)]
9#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
10pub struct DomainId(u32);
11
12impl DomainId {
13 pub const ZERO: Self = Self(0);
15
16 pub const fn get(self) -> u32 {
18 self.0
19 }
20}
21
22impl TryFrom<u32> for DomainId {
23 type Error = UnsupportedDomainId;
24
25 fn try_from(requested: u32) -> Result<Self, Self::Error> {
26 match requested {
27 0 => Ok(Self::ZERO),
28 requested => Err(UnsupportedDomainId { requested }),
29 }
30 }
31}
32
33#[derive(Debug, Copy, Clone, Eq, PartialEq)]
35pub struct UnsupportedDomainId {
36 requested: u32,
37}
38
39impl UnsupportedDomainId {
40 pub const fn requested(self) -> u32 {
42 self.requested
43 }
44}
45
46impl fmt::Display for UnsupportedDomainId {
47 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(formatter, "unsupported domain ID: {}", self.requested)
49 }
50}
51
52impl std::error::Error for UnsupportedDomainId {}
53
54#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
59pub struct Place {
60 rank: i32,
61 domain_id: DomainId,
62}
63
64impl Place {
65 pub fn new(rank: i32, domain_id: DomainId) -> Result<Self, NegativeRank> {
67 if rank < 0 {
68 return Err(NegativeRank { requested: rank });
69 }
70 Ok(Self { rank, domain_id })
71 }
72
73 pub const fn rank(self) -> i32 {
75 self.rank
76 }
77
78 pub const fn domain_id(self) -> DomainId {
80 self.domain_id
81 }
82}
83
84#[derive(Debug, Copy, Clone, Eq, PartialEq)]
86pub struct NegativeRank {
87 requested: i32,
88}
89
90impl NegativeRank {
91 pub const fn requested(self) -> i32 {
93 self.requested
94 }
95}
96
97impl fmt::Display for NegativeRank {
98 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99 write!(formatter, "rank must be non-negative: {}", self.requested)
100 }
101}
102
103impl std::error::Error for NegativeRank {}
104
105#[derive(Debug, Copy, Clone, Eq, PartialEq)]
107pub enum LocalMode {
108 Sequential,
110 Outer,
113 Inner,
116}
117
118#[cfg(feature = "rayon")]
119#[derive(Debug, Copy, Clone, Eq, PartialEq)]
121pub enum PoolOwnership {
122 Managed,
124 External,
126}
127
128#[cfg(feature = "rayon")]
129#[derive(Debug, Copy, Clone, Eq, PartialEq)]
131pub enum PlacementStatus {
132 Verified,
134 CallerDeclared,
136}
137
138#[cfg(feature = "rayon")]
139pub enum DomainBuildError {
141 EmptyCpuSet,
142 DuplicateCpu {
143 cpu: usize,
144 },
145 ZeroWorkers,
146 TooManyWorkers {
147 worker_count: usize,
148 cpu_count: usize,
149 },
150 CpuOutOfRange {
151 cpu: usize,
152 cpu_set_size: usize,
153 },
154 CpuNotAllowed {
155 cpu: usize,
156 },
157 AffinityQuery {
158 errno: i32,
159 },
160 PoolBuild {
161 message: String,
162 },
163 PinFailed {
164 worker_index: usize,
165 cpu: usize,
166 message: String,
167 },
168 StartTimeout {
169 expected: usize,
170 received: usize,
171 },
172 InvalidReport {
173 worker_index: usize,
174 cpu: usize,
175 },
176 PoolSizeMismatch {
177 expected: usize,
178 actual: usize,
179 },
180}
181
182#[cfg(feature = "rayon")]
183impl fmt::Debug for DomainBuildError {
184 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185 match self {
186 Self::EmptyCpuSet => formatter.write_str("EmptyCpuSet"),
187 Self::DuplicateCpu { cpu } => formatter
188 .debug_struct("DuplicateCpu")
189 .field("cpu", cpu)
190 .finish(),
191 Self::ZeroWorkers => formatter.write_str("ZeroWorkers"),
192 Self::TooManyWorkers {
193 worker_count,
194 cpu_count,
195 } => formatter
196 .debug_struct("TooManyWorkers")
197 .field("worker_count", worker_count)
198 .field("cpu_count", cpu_count)
199 .finish(),
200 Self::CpuOutOfRange { cpu, cpu_set_size } => formatter
201 .debug_struct("CpuOutOfRange")
202 .field("cpu", cpu)
203 .field("cpu_set_size", cpu_set_size)
204 .finish(),
205 Self::CpuNotAllowed { cpu } => formatter
206 .debug_struct("CpuNotAllowed")
207 .field("cpu", cpu)
208 .finish(),
209 Self::AffinityQuery { errno } => formatter
210 .debug_struct("AffinityQuery")
211 .field("errno", errno)
212 .finish(),
213 Self::PoolBuild { message } => formatter
214 .debug_struct("PoolBuild")
215 .field("message", message)
216 .finish(),
217 Self::PinFailed {
218 worker_index,
219 cpu,
220 message,
221 } => formatter
222 .debug_struct("PinFailed")
223 .field("worker_index", worker_index)
224 .field("cpu", cpu)
225 .field("message", message)
226 .finish(),
227 Self::StartTimeout { expected, received } => formatter
228 .debug_struct("StartTimeout")
229 .field("expected", expected)
230 .field("received", received)
231 .finish(),
232 Self::InvalidReport { worker_index, cpu } => formatter
233 .debug_struct("InvalidReport")
234 .field("worker_index", worker_index)
235 .field("cpu", cpu)
236 .finish(),
237 Self::PoolSizeMismatch { expected, actual } => formatter
238 .debug_struct("PoolSizeMismatch")
239 .field("expected", expected)
240 .field("actual", actual)
241 .finish(),
242 }
243 }
244}
245
246#[cfg(feature = "rayon")]
247impl fmt::Display for DomainBuildError {
248 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
249 match self {
250 Self::EmptyCpuSet => formatter.write_str("CPU set must not be empty"),
251 Self::DuplicateCpu { cpu } => write!(formatter, "CPU set contains duplicate CPU {cpu}"),
252 Self::ZeroWorkers => formatter.write_str("worker count must be greater than zero"),
253 Self::TooManyWorkers {
254 worker_count,
255 cpu_count,
256 } => write!(
257 formatter,
258 "worker count {worker_count} exceeds CPU set size {cpu_count}"
259 ),
260 Self::CpuOutOfRange { cpu, cpu_set_size } => {
261 write!(
262 formatter,
263 "CPU {cpu} is outside CPU set size {cpu_set_size}"
264 )
265 }
266 Self::CpuNotAllowed { cpu } => write!(formatter, "CPU {cpu} is not process-allowed"),
267 Self::AffinityQuery { errno } => write!(
268 formatter,
269 "could not query process affinity (errno {errno})"
270 ),
271 Self::PoolBuild { message } => {
272 write!(formatter, "could not build Rayon pool: {message}")
273 }
274 Self::PinFailed {
275 worker_index,
276 cpu,
277 message,
278 } => write!(
279 formatter,
280 "worker {worker_index} could not be placed on CPU {cpu}: {message}"
281 ),
282 Self::StartTimeout { expected, received } => write!(
283 formatter,
284 "timed out waiting for Rayon worker startup reports ({received}/{expected})"
285 ),
286 Self::InvalidReport { worker_index, cpu } => write!(
287 formatter,
288 "invalid Rayon worker startup report for worker {worker_index} on CPU {cpu}"
289 ),
290 Self::PoolSizeMismatch { expected, actual } => write!(
291 formatter,
292 "external Rayon pool has {actual} workers, expected {expected}"
293 ),
294 }
295 }
296}
297
298#[cfg(feature = "rayon")]
299impl std::error::Error for DomainBuildError {}
300
301pub struct Domain {
321 id: DomainId,
322 worker_count: usize,
323 running: AtomicBool,
324 #[cfg(feature = "rayon")]
325 pool: Option<std::sync::Arc<rayon::ThreadPool>>,
326 #[cfg(feature = "rayon")]
327 cpu_set: Option<Vec<usize>>,
328 #[cfg(feature = "rayon")]
329 ownership: Option<PoolOwnership>,
330 #[cfg(feature = "rayon")]
331 placement: Option<PlacementStatus>,
332}
333
334impl fmt::Debug for Domain {
335 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
336 let mut debug = formatter.debug_struct("Domain");
337 debug
338 .field("id", &self.id)
339 .field("worker_count", &self.worker_count)
340 .field("running", &self.running.load(Ordering::Relaxed));
341 #[cfg(feature = "rayon")]
342 debug
343 .field("has_pool", &self.pool.is_some())
344 .field("cpu_set", &self.cpu_set)
345 .field("ownership", &self.ownership)
346 .field("placement", &self.placement);
347 debug.finish()
348 }
349}
350
351impl Domain {
352 pub fn sequential() -> Self {
355 Self {
356 id: DomainId::ZERO,
357 worker_count: 1,
358 running: AtomicBool::new(false),
359 #[cfg(feature = "rayon")]
360 pool: None,
361 #[cfg(feature = "rayon")]
362 cpu_set: None,
363 #[cfg(feature = "rayon")]
364 ownership: None,
365 #[cfg(feature = "rayon")]
366 placement: None,
367 }
368 }
369
370 pub const fn id(&self) -> DomainId {
372 self.id
373 }
374
375 pub const fn worker_count(&self) -> usize {
377 self.worker_count
378 }
379
380 pub fn try_admit(&self) -> Result<DomainAdmission<'_>, DomainBusy> {
382 self.running
383 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
384 .map(|_| DomainAdmission { domain: self })
385 .map_err(|_| DomainBusy)
386 }
387
388 #[cfg(feature = "rayon")]
389 pub fn managed(cpu_set: Vec<usize>, worker_count: usize) -> Result<Self, DomainBuildError> {
391 validate_cpu_set(&cpu_set, worker_count)?;
392 #[cfg(target_os = "linux")]
393 {
394 validate_linux_cpu_range(&cpu_set)?;
395 let allowed = process_allowed_cpus()?;
396 for &cpu in &cpu_set {
397 if !allowed.contains(&cpu) {
398 return Err(DomainBuildError::CpuNotAllowed { cpu });
399 }
400 }
401 }
402 build_managed(cpu_set, worker_count, std::sync::Arc::new(DefaultPinner))
403 }
404
405 #[cfg(feature = "rayon")]
406 pub fn external(
409 pool: std::sync::Arc<rayon::ThreadPool>,
410 declared_cpu_set: Vec<usize>,
411 worker_count: usize,
412 ) -> Result<Self, DomainBuildError> {
413 validate_cpu_set(&declared_cpu_set, worker_count)?;
414 #[cfg(target_os = "linux")]
415 validate_linux_cpu_range(&declared_cpu_set)?;
416 let actual = pool.current_num_threads();
417 if actual != worker_count {
418 return Err(DomainBuildError::PoolSizeMismatch {
419 expected: worker_count,
420 actual,
421 });
422 }
423 Ok(Self {
424 id: DomainId::ZERO,
425 worker_count,
426 running: AtomicBool::new(false),
427 pool: Some(pool),
428 cpu_set: Some(declared_cpu_set),
429 ownership: Some(PoolOwnership::External),
430 placement: Some(PlacementStatus::CallerDeclared),
431 })
432 }
433
434 #[cfg(feature = "rayon")]
435 pub fn cpu_set(&self) -> Option<&[usize]> {
437 self.cpu_set.as_deref()
438 }
439
440 #[cfg(feature = "rayon")]
441 pub fn pool_ownership(&self) -> Option<PoolOwnership> {
443 self.ownership
444 }
445
446 #[cfg(feature = "rayon")]
447 pub fn placement_status(&self) -> Option<PlacementStatus> {
449 self.placement
450 }
451
452 #[cfg(feature = "rayon")]
453 pub fn rayon_pool_handle(&self) -> Option<std::sync::Arc<rayon::ThreadPool>> {
473 self.pool.clone()
474 }
475
476 #[cfg(feature = "rayon")]
477 pub(crate) fn rayon_pool(&self) -> Option<&std::sync::Arc<rayon::ThreadPool>> {
478 self.pool.as_ref()
479 }
480}
481
482#[cfg(feature = "rayon")]
483fn validate_cpu_set(cpu_set: &[usize], worker_count: usize) -> Result<(), DomainBuildError> {
484 if cpu_set.is_empty() {
485 return Err(DomainBuildError::EmptyCpuSet);
486 }
487 let mut seen = std::collections::HashSet::with_capacity(cpu_set.len());
488 for &cpu in cpu_set {
489 if !seen.insert(cpu) {
490 return Err(DomainBuildError::DuplicateCpu { cpu });
491 }
492 }
493 if worker_count == 0 {
494 return Err(DomainBuildError::ZeroWorkers);
495 }
496 if worker_count > cpu_set.len() {
497 return Err(DomainBuildError::TooManyWorkers {
498 worker_count,
499 cpu_count: cpu_set.len(),
500 });
501 }
502 Ok(())
503}
504
505#[cfg(all(feature = "rayon", target_os = "linux"))]
506fn validate_linux_cpu_range(cpu_set: &[usize]) -> Result<(), DomainBuildError> {
507 let cpu_set_size = libc::CPU_SETSIZE as usize;
508 for &cpu in cpu_set {
509 if cpu >= cpu_set_size {
510 return Err(DomainBuildError::CpuOutOfRange { cpu, cpu_set_size });
511 }
512 }
513 Ok(())
514}
515
516#[cfg(all(feature = "rayon", target_os = "linux"))]
517fn process_allowed_cpus() -> Result<std::collections::HashSet<usize>, DomainBuildError> {
518 let mut allowed = unsafe { std::mem::zeroed::<libc::cpu_set_t>() };
519 let result =
520 unsafe { libc::sched_getaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &mut allowed) };
521 if result != 0 {
522 return Err(DomainBuildError::AffinityQuery {
523 errno: std::io::Error::last_os_error().raw_os_error().unwrap_or(-1),
524 });
525 }
526 let mut cpus = std::collections::HashSet::new();
527 for cpu in 0..libc::CPU_SETSIZE as usize {
528 if unsafe { libc::CPU_ISSET(cpu, &allowed) } {
529 cpus.insert(cpu);
530 }
531 }
532 Ok(cpus)
533}
534
535#[cfg(feature = "rayon")]
536trait Pinner: Send + Sync {
537 fn pin_and_verify(&self, cpu: usize) -> Result<(), String>;
538}
539
540#[cfg(feature = "rayon")]
541struct DefaultPinner;
542
543#[cfg(all(feature = "rayon", target_os = "linux"))]
544impl Pinner for DefaultPinner {
545 fn pin_and_verify(&self, cpu: usize) -> Result<(), String> {
546 let mut set = unsafe { std::mem::zeroed::<libc::cpu_set_t>() };
547 unsafe { libc::CPU_ZERO(&mut set) };
548 unsafe { libc::CPU_SET(cpu, &mut set) };
549 let result = unsafe {
550 libc::pthread_setaffinity_np(
551 libc::pthread_self(),
552 std::mem::size_of::<libc::cpu_set_t>(),
553 &set,
554 )
555 };
556 if result != 0 {
557 return Err(format!("pthread_setaffinity_np failed (errno {result})"));
558 }
559 let actual = unsafe { libc::sched_getcpu() };
560 if actual != cpu as i32 {
561 return Err(format!("worker ran on CPU {actual}"));
562 }
563 Ok(())
564 }
565}
566
567#[cfg(all(feature = "rayon", not(target_os = "linux")))]
568impl Pinner for DefaultPinner {
569 fn pin_and_verify(&self, _cpu: usize) -> Result<(), String> {
570 Ok(())
571 }
572}
573
574#[cfg(feature = "rayon")]
575struct WorkerReport {
576 index: usize,
577 cpu: usize,
578 result: Result<(), String>,
579}
580
581#[cfg(feature = "rayon")]
582fn build_managed(
583 cpu_set: Vec<usize>,
584 worker_count: usize,
585 pinner: std::sync::Arc<dyn Pinner>,
586) -> Result<Domain, DomainBuildError> {
587 let expected_cpus = cpu_set.clone();
588 let start_cpus = expected_cpus.clone();
589 let (sender, receiver) = std::sync::mpsc::channel();
590 let builder = rayon::ThreadPoolBuilder::new()
591 .num_threads(worker_count)
592 .thread_name(|index| format!("hataori-worker-{index}"))
593 .start_handler(move |index| {
594 let cpu = start_cpus.get(index).copied().unwrap_or(usize::MAX);
595 let result = if cpu == usize::MAX {
596 Err(String::from("worker index is outside the declared CPU set"))
597 } else {
598 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
599 pinner.pin_and_verify(cpu)
600 }))
601 .unwrap_or_else(|_| Err(String::from("worker pinner panicked")))
602 };
603 let _ = sender.send(WorkerReport { index, cpu, result });
604 });
605 let pool = builder
606 .build()
607 .map_err(|error| DomainBuildError::PoolBuild {
608 message: error.to_string(),
609 })?;
610
611 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
612 let mut seen = vec![false; worker_count];
613 for received in 0..worker_count {
614 let report = match receiver
615 .recv_timeout(deadline.saturating_duration_since(std::time::Instant::now()))
616 {
617 Ok(report) => report,
618 Err(_) => {
619 drop(pool);
620 return Err(DomainBuildError::StartTimeout {
621 expected: worker_count,
622 received,
623 });
624 }
625 };
626 if report.index >= worker_count
627 || seen[report.index]
628 || expected_cpus.get(report.index).copied() != Some(report.cpu)
629 {
630 drop(pool);
631 return Err(DomainBuildError::InvalidReport {
632 worker_index: report.index,
633 cpu: report.cpu,
634 });
635 }
636 seen[report.index] = true;
637 if let Err(message) = report.result {
638 drop(pool);
639 return Err(DomainBuildError::PinFailed {
640 worker_index: report.index,
641 cpu: report.cpu,
642 message,
643 });
644 }
645 }
646
647 Ok(Domain {
648 id: DomainId::ZERO,
649 worker_count,
650 running: AtomicBool::new(false),
651 pool: Some(std::sync::Arc::new(pool)),
652 cpu_set: Some(cpu_set),
653 ownership: Some(PoolOwnership::Managed),
654 placement: Some(if cfg!(target_os = "linux") {
655 PlacementStatus::Verified
656 } else {
657 PlacementStatus::CallerDeclared
658 }),
659 })
660}
661
662#[must_use]
667#[derive(Debug)]
668pub struct DomainAdmission<'a> {
669 domain: &'a Domain,
670}
671
672impl Drop for DomainAdmission<'_> {
673 fn drop(&mut self) {
674 self.domain.running.store(false, Ordering::Release);
675 }
676}
677
678#[derive(Debug, Copy, Clone, Eq, PartialEq)]
680pub struct DomainBusy;
681
682impl fmt::Display for DomainBusy {
683 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
684 formatter.write_str("domain is busy")
685 }
686}
687
688impl std::error::Error for DomainBusy {}
689
690#[cfg(test)]
691mod tests;