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 {
692 use super::*;
693 use std::panic::{catch_unwind, AssertUnwindSafe};
694 use std::sync::{mpsc, Barrier};
695 use std::thread;
696
697 #[test]
698 fn domain_id_zero_conversion_and_nonzero_error() {
699 let zero = DomainId::try_from(0).unwrap();
700 assert_eq!(zero, DomainId::ZERO);
701 assert_eq!(zero.get(), 0);
702
703 let error = DomainId::try_from(1).unwrap_err();
704 assert_eq!(error.requested(), 1);
705 assert_eq!(error.to_string(), "unsupported domain ID: 1");
706 }
707
708 #[test]
709 fn place_valid_getters_and_negative_rank_error() {
710 let place = Place::new(3, DomainId::ZERO).unwrap();
711 assert_eq!(place.rank(), 3);
712 assert_eq!(place.domain_id(), DomainId::ZERO);
713
714 let error = Place::new(-1, DomainId::ZERO).unwrap_err();
715 assert_eq!(error.requested(), -1);
716 assert_eq!(error.to_string(), "rank must be non-negative: -1");
717 }
718
719 #[test]
720 fn sequential_domain_has_zero_id_and_one_worker() {
721 let domain = Domain::sequential();
722 assert_eq!(domain.id(), DomainId::ZERO);
723 assert_eq!(domain.worker_count(), 1);
724 #[cfg(feature = "rayon")]
725 {
726 assert_eq!(domain.cpu_set(), None);
727 assert_eq!(domain.pool_ownership(), None);
728 assert_eq!(domain.placement_status(), None);
729 assert!(domain.pool.is_none());
730 }
731 }
732
733 #[test]
734 fn nested_admission_is_busy_and_reacquires_after_drop() {
735 let domain = Domain::sequential();
736 let admission = domain.try_admit().unwrap();
737 assert_eq!(domain.try_admit().unwrap_err(), DomainBusy);
738 drop(admission);
739 assert!(domain.try_admit().is_ok());
740 }
741
742 fn returns_error(domain: &Domain) -> Result<(), &'static str> {
743 let _admission = domain.try_admit().unwrap();
744 Err("ordinary error")
745 }
746
747 #[test]
748 fn admission_releases_after_helper_returns_error() {
749 let domain = Domain::sequential();
750 assert_eq!(returns_error(&domain), Err("ordinary error"));
751 assert!(domain.try_admit().is_ok());
752 }
753
754 #[test]
755 fn admission_releases_during_unwind() {
756 let domain = Domain::sequential();
757 let result = catch_unwind(AssertUnwindSafe(|| {
758 let _admission = domain.try_admit().unwrap();
759 panic!("test panic");
760 }));
761 assert!(result.is_err());
762 assert!(domain.try_admit().is_ok());
763 }
764
765 #[test]
766 fn scoped_cross_thread_admission_is_nonblocking_and_reacquires() {
767 let domain = Domain::sequential();
768 let barrier = Barrier::new(2);
769 let (release_tx, release_rx) = mpsc::channel();
770 let (busy_tx, busy_rx) = mpsc::channel();
771 let (released_tx, released_rx) = mpsc::channel();
772 let (success_tx, success_rx) = mpsc::channel();
773 let domain_ref = &domain;
774
775 thread::scope(|scope| {
776 let holder_domain = domain_ref;
777 let holder_barrier = &barrier;
778 scope.spawn(move || {
779 let admission = holder_domain.try_admit().unwrap();
780 holder_barrier.wait();
781 release_rx.recv().unwrap();
782 drop(admission);
783 released_tx.send(()).unwrap();
784 });
785
786 barrier.wait();
787 let contender_domain = domain_ref;
788 scope.spawn(move || {
789 busy_tx
790 .send(matches!(contender_domain.try_admit(), Err(DomainBusy)))
791 .unwrap();
792 release_tx.send(()).unwrap();
793 released_rx.recv().unwrap();
794 success_tx
795 .send(contender_domain.try_admit().is_ok())
796 .unwrap();
797 });
798
799 assert!(busy_rx.recv().unwrap());
800 assert!(success_rx.recv().unwrap());
801 });
802 }
803
804 #[cfg(feature = "rayon")]
805 struct FailingPinner;
806
807 #[cfg(feature = "rayon")]
808 impl Pinner for FailingPinner {
809 fn pin_and_verify(&self, _cpu: usize) -> Result<(), String> {
810 Err(String::from("injected failure"))
811 }
812 }
813
814 #[cfg(feature = "rayon")]
815 #[test]
816 fn managed_rejects_invalid_worker_configuration() {
817 assert!(matches!(
818 Domain::managed(Vec::new(), 1),
819 Err(DomainBuildError::EmptyCpuSet)
820 ));
821 assert!(matches!(
822 Domain::managed(vec![0, 0], 1),
823 Err(DomainBuildError::DuplicateCpu { cpu: 0 })
824 ));
825 assert!(matches!(
826 Domain::managed(vec![0], 0),
827 Err(DomainBuildError::ZeroWorkers)
828 ));
829 assert!(matches!(
830 Domain::managed(vec![0], 2),
831 Err(DomainBuildError::TooManyWorkers {
832 worker_count: 2,
833 cpu_count: 1
834 })
835 ));
836 }
837
838 #[cfg(feature = "rayon")]
839 #[test]
840 fn external_rejects_pool_size_mismatch() {
841 let pool = std::sync::Arc::new(
842 rayon::ThreadPoolBuilder::new()
843 .num_threads(2)
844 .build()
845 .unwrap(),
846 );
847 assert!(matches!(
848 Domain::external(pool, vec![0], 1),
849 Err(DomainBuildError::PoolSizeMismatch {
850 expected: 1,
851 actual: 2
852 })
853 ));
854 }
855
856 #[cfg(feature = "rayon")]
857 #[test]
858 fn external_does_not_own_or_pin_caller_pool() {
859 let starts = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
860 let starts_in_handler = std::sync::Arc::clone(&starts);
861 let caller_pool = std::sync::Arc::new(
862 rayon::ThreadPoolBuilder::new()
863 .num_threads(1)
864 .start_handler(move |_| {
865 starts_in_handler.fetch_add(1, Ordering::Relaxed);
866 })
867 .build()
868 .unwrap(),
869 );
870 let domain = Domain::external(std::sync::Arc::clone(&caller_pool), vec![0], 1).unwrap();
871 assert_eq!(domain.pool_ownership(), Some(PoolOwnership::External));
872 assert_eq!(
873 domain.placement_status(),
874 Some(PlacementStatus::CallerDeclared)
875 );
876 drop(domain);
877
878 caller_pool.install(|| assert_eq!(rayon::current_num_threads(), 1));
879 assert_eq!(starts.load(Ordering::Relaxed), 1);
880 }
881
882 #[cfg(all(feature = "rayon", target_os = "linux"))]
883 #[test]
884 fn managed_rejects_linux_cpu_bounds_and_process_disallowed_cpu() {
885 let out_of_range = libc::CPU_SETSIZE as usize;
886 assert!(matches!(
887 Domain::managed(vec![out_of_range], 1),
888 Err(DomainBuildError::CpuOutOfRange {
889 cpu,
890 cpu_set_size
891 }) if cpu == out_of_range && cpu_set_size == out_of_range
892 ));
893
894 let allowed = process_allowed_cpus().unwrap();
895 let disallowed = (0..libc::CPU_SETSIZE as usize).find(|cpu| !allowed.contains(cpu));
896 let Some(disallowed) = disallowed else {
897 return;
898 };
899 assert!(matches!(
900 Domain::managed(vec![disallowed], 1),
901 Err(DomainBuildError::CpuNotAllowed { cpu }) if cpu == disallowed
902 ));
903 }
904
905 #[cfg(feature = "rayon")]
906 #[test]
907 fn managed_pin_failure_is_typed_and_does_not_panic() {
908 let result = catch_unwind(AssertUnwindSafe(|| {
909 build_managed(vec![0], 1, std::sync::Arc::new(FailingPinner))
910 }))
911 .unwrap();
912 assert!(matches!(result, Err(DomainBuildError::PinFailed { .. })));
913 }
914
915 #[cfg(all(feature = "rayon", target_os = "linux"))]
916 #[test]
917 fn managed_workers_are_on_distinct_allowed_cpus() {
918 let allowed = process_allowed_cpus().unwrap();
919 assert!(!allowed.is_empty());
920 let cpus: Vec<_> = allowed.iter().copied().take(2).collect();
921 let worker_count = cpus.len().max(1);
922 let domain = Domain::managed(cpus.clone(), worker_count).unwrap();
923 assert_eq!(domain.placement_status(), Some(PlacementStatus::Verified));
924 let observed = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
925 let copy = std::sync::Arc::clone(&observed);
926 domain.pool.as_ref().unwrap().broadcast(|_| {
927 let cpu = unsafe { libc::sched_getcpu() } as usize;
928 copy.lock().unwrap().push(cpu);
929 });
930 let observed = observed.lock().unwrap();
931 assert_eq!(observed.len(), worker_count);
932 assert!(observed.iter().all(|cpu| cpus.contains(cpu)));
933 let distinct: std::collections::HashSet<_> = observed.iter().copied().collect();
934 assert_eq!(distinct.len(), worker_count);
935 }
936
937 #[cfg(all(feature = "rayon", not(target_os = "linux")))]
938 #[test]
939 fn managed_non_linux_placement_is_caller_declared() {
940 let domain = Domain::managed(vec![0], 1).unwrap();
941 assert_eq!(
942 domain.placement_status(),
943 Some(PlacementStatus::CallerDeclared)
944 );
945 }
946}