tenferro_cpu/resource_domain.rs
1use std::num::NonZeroUsize;
2use std::sync::atomic::AtomicBool;
3use std::sync::Arc;
4
5use thiserror::Error;
6
7use crate::{
8 CpuDomainExecutor, CpuDomainExecutorCapabilities, CpuDomainId, CpuPlacementGuarantee, CpuSet,
9 ResolvedCpuPlacement,
10};
11
12/// Ownership class of a CPU resource domain.
13///
14/// # Examples
15///
16/// ```rust
17/// use tenferro_cpu::CpuDomainOwnership;
18///
19/// assert_ne!(
20/// CpuDomainOwnership::Managed,
21/// CpuDomainOwnership::ExternalManaged,
22/// );
23/// ```
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum CpuDomainOwnership {
26 /// Tenferro constructed and owns the resource domain.
27 Managed,
28 /// The application supplied and owns the executor resource policy.
29 ExternalManaged,
30}
31
32/// Admission contract for one CPU resource domain.
33///
34/// # Examples
35///
36/// ```rust
37/// use tenferro_cpu::CpuAdmissionMode;
38///
39/// assert_ne!(
40/// CpuAdmissionMode::CooperativeCpuSet,
41/// CpuAdmissionMode::CallerManaged,
42/// );
43/// ```
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum CpuAdmissionMode {
46 /// tenferro arbitrates the domain's declared CPU set with other CPU work.
47 CooperativeCpuSet,
48 /// The caller owns cross-domain admission; tenferro guards only this domain.
49 CallerManaged,
50}
51
52#[derive(Debug)]
53enum CpuDomainAdmission {
54 CooperativeCpuSet {
55 placement: ResolvedCpuPlacement,
56 guarantee: CpuPlacementGuarantee,
57 },
58 CallerManaged {
59 active: Arc<AtomicBool>,
60 },
61}
62
63/// Typed failure to construct an externally managed CPU resource domain.
64///
65/// # Examples
66///
67/// ```rust
68/// use tenferro_cpu::ExternalCpuDomainError;
69///
70/// let error = ExternalCpuDomainError::ThreadBudgetExceedsWorkerCount {
71/// thread_budget: 4,
72/// worker_count: 2,
73/// };
74/// assert!(error.to_string().contains("4"));
75/// ```
76#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
77pub enum ExternalCpuDomainError {
78 /// The resolved placement contains no logical CPUs.
79 #[error("external CPU domain placement must contain at least one CPU")]
80 EmptyPlacementCpuSet,
81 /// The executor reported no workers.
82 #[error("external CPU domain executor must report at least one worker")]
83 ZeroExecutorWorkers,
84 /// The requested thread budget is larger than the executor worker count.
85 #[error(
86 "external CPU domain thread budget {thread_budget} exceeds executor worker count {worker_count}"
87 )]
88 ThreadBudgetExceedsWorkerCount {
89 /// Requested maximum number of participating threads.
90 thread_budget: usize,
91 /// Workers reported by the supplied executor.
92 worker_count: usize,
93 },
94}
95
96#[derive(Debug)]
97pub(crate) struct CpuResourceDomain {
98 id: CpuDomainId,
99 admission: CpuDomainAdmission,
100 executor: Arc<dyn CpuDomainExecutor>,
101 thread_budget: NonZeroUsize,
102 ownership: CpuDomainOwnership,
103}
104
105impl CpuResourceDomain {
106 pub(crate) fn new(
107 id: CpuDomainId,
108 placement: ResolvedCpuPlacement,
109 executor: Arc<dyn CpuDomainExecutor>,
110 thread_budget: NonZeroUsize,
111 placement_guarantee: CpuPlacementGuarantee,
112 ownership: CpuDomainOwnership,
113 ) -> Self {
114 Self {
115 id,
116 admission: CpuDomainAdmission::CooperativeCpuSet {
117 placement,
118 guarantee: placement_guarantee,
119 },
120 executor,
121 thread_budget,
122 ownership,
123 }
124 }
125
126 fn new_caller_managed(
127 id: CpuDomainId,
128 executor: Arc<dyn CpuDomainExecutor>,
129 thread_budget: NonZeroUsize,
130 ) -> Self {
131 Self {
132 id,
133 admission: CpuDomainAdmission::CallerManaged {
134 active: Arc::new(AtomicBool::new(false)),
135 },
136 executor,
137 thread_budget,
138 ownership: CpuDomainOwnership::ExternalManaged,
139 }
140 }
141
142 pub(crate) fn id(&self) -> CpuDomainId {
143 self.id
144 }
145
146 pub(crate) fn admission_mode(&self) -> CpuAdmissionMode {
147 match self.admission {
148 CpuDomainAdmission::CooperativeCpuSet { .. } => CpuAdmissionMode::CooperativeCpuSet,
149 CpuDomainAdmission::CallerManaged { .. } => CpuAdmissionMode::CallerManaged,
150 }
151 }
152
153 pub(crate) fn placement(&self) -> Option<&ResolvedCpuPlacement> {
154 match &self.admission {
155 CpuDomainAdmission::CooperativeCpuSet { placement, .. } => Some(placement),
156 CpuDomainAdmission::CallerManaged { .. } => None,
157 }
158 }
159
160 pub(crate) fn cpus(&self) -> Option<&CpuSet> {
161 self.placement().map(ResolvedCpuPlacement::cpus)
162 }
163
164 pub(crate) fn caller_managed_active(&self) -> Option<Arc<AtomicBool>> {
165 match &self.admission {
166 CpuDomainAdmission::CallerManaged { active } => Some(Arc::clone(active)),
167 CpuDomainAdmission::CooperativeCpuSet { .. } => None,
168 }
169 }
170
171 pub(crate) fn executor(&self) -> &Arc<dyn CpuDomainExecutor> {
172 &self.executor
173 }
174
175 pub(crate) fn thread_budget(&self) -> NonZeroUsize {
176 self.thread_budget
177 }
178
179 pub(crate) fn placement_guarantee(&self) -> Option<CpuPlacementGuarantee> {
180 match self.admission {
181 CpuDomainAdmission::CooperativeCpuSet { guarantee, .. } => Some(guarantee),
182 CpuDomainAdmission::CallerManaged { .. } => None,
183 }
184 }
185
186 pub(crate) fn ownership(&self) -> CpuDomainOwnership {
187 self.ownership
188 }
189
190 pub(crate) fn executor_capabilities(&self) -> CpuDomainExecutorCapabilities {
191 self.executor().capabilities()
192 }
193}
194
195/// Caller-supplied descriptor for one externally managed CPU resource domain.
196///
197/// The descriptor retains the supplied executor without replacing its pool or
198/// changing its affinity claim. Registration and process-CPU-set validation
199/// are performed later by [`crate::CpuBackend`].
200///
201/// # Examples
202///
203/// ```rust
204/// use std::num::NonZeroUsize;
205/// use std::sync::Arc;
206/// use tenferro_cpu::{
207/// CpuContext, CpuDomainOwnership, CpuId, CpuPlacementGuarantee, CpuSet,
208/// ExternalCpuDomain, ResolvedCpuPlacement,
209/// };
210/// use tenferro_tensor::CpuDomainId;
211///
212/// let domain = ExternalCpuDomain::new(
213/// CpuDomainId::new(7),
214/// ResolvedCpuPlacement::AllAllowed {
215/// cpus: CpuSet::new([CpuId::new(0)])?,
216/// },
217/// Arc::new(CpuContext::with_threads(1)?),
218/// NonZeroUsize::new(1).unwrap(),
219/// CpuPlacementGuarantee::AdvisoryDeclared,
220/// )?;
221/// assert_eq!(domain.ownership(), CpuDomainOwnership::ExternalManaged);
222/// # Ok::<(), Box<dyn std::error::Error>>(())
223/// ```
224#[derive(Debug)]
225pub struct ExternalCpuDomain {
226 domain: CpuResourceDomain,
227}
228
229impl ExternalCpuDomain {
230 /// Construct one externally managed CPU resource-domain descriptor.
231 ///
232 /// The executor is retained for the complete descriptor lifetime. Exact
233 /// and advisory placement values remain caller declarations and do not
234 /// alter the executor's affinity capability.
235 ///
236 /// # Examples
237 ///
238 /// ```rust
239 /// use std::num::NonZeroUsize;
240 /// use std::sync::Arc;
241 /// use tenferro_cpu::{
242 /// CpuContext, CpuId, CpuPlacementGuarantee, CpuSet, ExternalCpuDomain,
243 /// ResolvedCpuPlacement,
244 /// };
245 /// use tenferro_tensor::CpuDomainId;
246 ///
247 /// let domain = ExternalCpuDomain::new(
248 /// CpuDomainId::new(3),
249 /// ResolvedCpuPlacement::AllAllowed {
250 /// cpus: CpuSet::new([CpuId::new(0)])?,
251 /// },
252 /// Arc::new(CpuContext::with_threads(1)?),
253 /// NonZeroUsize::new(1).unwrap(),
254 /// CpuPlacementGuarantee::ExactDeclared,
255 /// )?;
256 /// assert_eq!(domain.id(), CpuDomainId::new(3));
257 /// # Ok::<(), Box<dyn std::error::Error>>(())
258 /// ```
259 ///
260 /// # Errors
261 ///
262 /// Returns [`ExternalCpuDomainError::EmptyPlacementCpuSet`] for an empty
263 /// resolved CPU set, [`ExternalCpuDomainError::ZeroExecutorWorkers`] when
264 /// the executor reports no workers, or
265 /// [`ExternalCpuDomainError::ThreadBudgetExceedsWorkerCount`] when
266 /// `thread_budget` is greater than the executor's worker count.
267 pub fn new(
268 id: CpuDomainId,
269 placement: ResolvedCpuPlacement,
270 executor: Arc<dyn CpuDomainExecutor>,
271 thread_budget: NonZeroUsize,
272 placement_guarantee: CpuPlacementGuarantee,
273 ) -> Result<Self, ExternalCpuDomainError> {
274 let worker_count = executor.capabilities().worker_count.get();
275 validate_external_domain_config(Some(placement.cpus().len()), worker_count, thread_budget)?;
276 Ok(Self {
277 domain: CpuResourceDomain::new(
278 id,
279 placement,
280 executor,
281 thread_budget,
282 placement_guarantee,
283 CpuDomainOwnership::ExternalManaged,
284 ),
285 })
286 }
287
288 /// Construct a caller-managed domain without declaring a CPU set.
289 ///
290 /// The caller owns admission between distinct caller-managed domains. tenferro
291 /// retains `executor`, rejects concurrent public entry into this domain, and
292 /// never constructs or shuts down another executor.
293 ///
294 /// # Examples
295 ///
296 /// ```rust
297 /// use std::num::NonZeroUsize;
298 /// use std::sync::Arc;
299 /// use tenferro_cpu::{
300 /// CpuAdmissionMode, ExternalCpuDomain, RayonCpuDomainExecutor,
301 /// };
302 /// use tenferro_tensor::CpuDomainId;
303 ///
304 /// let pool = Arc::new(rayon::ThreadPoolBuilder::new().num_threads(2).build()?);
305 /// let executor = Arc::new(RayonCpuDomainExecutor::new(Arc::clone(&pool)));
306 /// let domain = ExternalCpuDomain::new_caller_managed(
307 /// CpuDomainId::new(9),
308 /// executor,
309 /// NonZeroUsize::new(2).unwrap(),
310 /// )?;
311 /// assert_eq!(domain.admission_mode(), CpuAdmissionMode::CallerManaged);
312 /// assert!(domain.placement().is_none());
313 /// # Ok::<(), Box<dyn std::error::Error>>(())
314 /// ```
315 ///
316 /// # Errors
317 ///
318 /// Returns [`ExternalCpuDomainError::ZeroExecutorWorkers`] when the executor
319 /// reports no workers, or
320 /// [`ExternalCpuDomainError::ThreadBudgetExceedsWorkerCount`] when
321 /// `thread_budget` exceeds the executor worker count.
322 pub fn new_caller_managed(
323 id: CpuDomainId,
324 executor: Arc<dyn CpuDomainExecutor>,
325 thread_budget: NonZeroUsize,
326 ) -> Result<Self, ExternalCpuDomainError> {
327 let worker_count = executor.capabilities().worker_count.get();
328 validate_external_domain_config(None, worker_count, thread_budget)?;
329 Ok(Self {
330 domain: CpuResourceDomain::new_caller_managed(id, executor, thread_budget),
331 })
332 }
333
334 /// Return the caller-stable identity of this CPU domain.
335 ///
336 /// # Examples
337 ///
338 /// ```rust
339 /// use tenferro_cpu::ExternalCpuDomain;
340 /// use tenferro_tensor::CpuDomainId;
341 ///
342 /// let _id: fn(&ExternalCpuDomain) -> CpuDomainId = ExternalCpuDomain::id;
343 /// ```
344 pub fn id(&self) -> CpuDomainId {
345 self.domain.id()
346 }
347
348 /// Return the declared resolved placement, if this domain uses CPU-set admission.
349 ///
350 /// # Examples
351 ///
352 /// ```rust
353 /// use std::num::NonZeroUsize;
354 /// use std::sync::Arc;
355 /// use tenferro_cpu::{CpuContext, ExternalCpuDomain};
356 /// use tenferro_tensor::CpuDomainId;
357 ///
358 /// let domain = ExternalCpuDomain::new_caller_managed(
359 /// CpuDomainId::new(1),
360 /// Arc::new(CpuContext::with_threads(1)?),
361 /// NonZeroUsize::MIN,
362 /// )?;
363 /// assert!(domain.placement().is_none());
364 /// # Ok::<(), Box<dyn std::error::Error>>(())
365 /// ```
366 pub fn placement(&self) -> Option<&ResolvedCpuPlacement> {
367 self.domain.placement()
368 }
369
370 /// Return the logical CPUs declared for CPU-set admission.
371 ///
372 /// # Examples
373 ///
374 /// ```rust
375 /// use std::num::NonZeroUsize;
376 /// use std::sync::Arc;
377 /// use tenferro_cpu::{CpuContext, ExternalCpuDomain};
378 /// use tenferro_tensor::CpuDomainId;
379 ///
380 /// let domain = ExternalCpuDomain::new_caller_managed(
381 /// CpuDomainId::new(1),
382 /// Arc::new(CpuContext::with_threads(1)?),
383 /// NonZeroUsize::MIN,
384 /// )?;
385 /// assert!(domain.cpus().is_none());
386 /// # Ok::<(), Box<dyn std::error::Error>>(())
387 /// ```
388 pub fn cpus(&self) -> Option<&CpuSet> {
389 self.domain.cpus()
390 }
391
392 /// Return this domain's admission contract.
393 ///
394 /// # Examples
395 ///
396 /// ```rust
397 /// use std::num::NonZeroUsize;
398 /// use std::sync::Arc;
399 /// use tenferro_cpu::{CpuAdmissionMode, CpuContext, ExternalCpuDomain};
400 /// use tenferro_tensor::CpuDomainId;
401 ///
402 /// let domain = ExternalCpuDomain::new_caller_managed(
403 /// CpuDomainId::new(1),
404 /// Arc::new(CpuContext::with_threads(1)?),
405 /// NonZeroUsize::MIN,
406 /// )?;
407 /// assert_eq!(domain.admission_mode(), CpuAdmissionMode::CallerManaged);
408 /// # Ok::<(), Box<dyn std::error::Error>>(())
409 /// ```
410 pub fn admission_mode(&self) -> CpuAdmissionMode {
411 self.domain.admission_mode()
412 }
413
414 /// Return the nonzero thread budget requested for tenferro work.
415 ///
416 /// # Examples
417 ///
418 /// ```rust
419 /// use std::num::NonZeroUsize;
420 /// use tenferro_cpu::ExternalCpuDomain;
421 ///
422 /// let _budget: fn(&ExternalCpuDomain) -> NonZeroUsize =
423 /// ExternalCpuDomain::thread_budget;
424 /// ```
425 pub fn thread_budget(&self) -> NonZeroUsize {
426 self.domain.thread_budget()
427 }
428
429 /// Return whether cooperative placement is an exact or advisory declaration.
430 ///
431 /// # Examples
432 ///
433 /// ```rust
434 /// use std::num::NonZeroUsize;
435 /// use std::sync::Arc;
436 /// use tenferro_cpu::{CpuContext, ExternalCpuDomain};
437 /// use tenferro_tensor::CpuDomainId;
438 ///
439 /// let domain = ExternalCpuDomain::new_caller_managed(
440 /// CpuDomainId::new(1),
441 /// Arc::new(CpuContext::with_threads(1)?),
442 /// NonZeroUsize::MIN,
443 /// )?;
444 /// assert!(domain.placement_guarantee().is_none());
445 /// # Ok::<(), Box<dyn std::error::Error>>(())
446 /// ```
447 pub fn placement_guarantee(&self) -> Option<CpuPlacementGuarantee> {
448 self.domain.placement_guarantee()
449 }
450
451 /// Return the external ownership diagnostic.
452 ///
453 /// # Examples
454 ///
455 /// ```rust
456 /// use tenferro_cpu::{CpuDomainOwnership, ExternalCpuDomain};
457 ///
458 /// let _ownership: fn(&ExternalCpuDomain) -> CpuDomainOwnership =
459 /// ExternalCpuDomain::ownership;
460 /// ```
461 pub fn ownership(&self) -> CpuDomainOwnership {
462 self.domain.ownership()
463 }
464
465 /// Return the supplied executor's immutable capability descriptor.
466 ///
467 /// # Examples
468 ///
469 /// ```rust
470 /// use tenferro_cpu::{CpuDomainExecutorCapabilities, ExternalCpuDomain};
471 ///
472 /// let _capabilities: fn(&ExternalCpuDomain) -> CpuDomainExecutorCapabilities =
473 /// ExternalCpuDomain::executor_capabilities;
474 /// ```
475 pub fn executor_capabilities(&self) -> CpuDomainExecutorCapabilities {
476 self.domain.executor_capabilities()
477 }
478}
479
480impl From<ExternalCpuDomain> for CpuResourceDomain {
481 fn from(domain: ExternalCpuDomain) -> Self {
482 domain.domain
483 }
484}
485
486fn validate_external_domain_config(
487 cpu_count: Option<usize>,
488 worker_count: usize,
489 thread_budget: NonZeroUsize,
490) -> Result<(), ExternalCpuDomainError> {
491 if cpu_count == Some(0) {
492 return Err(ExternalCpuDomainError::EmptyPlacementCpuSet);
493 }
494 if worker_count == 0 {
495 return Err(ExternalCpuDomainError::ZeroExecutorWorkers);
496 }
497 if thread_budget.get() > worker_count {
498 return Err(ExternalCpuDomainError::ThreadBudgetExceedsWorkerCount {
499 thread_budget: thread_budget.get(),
500 worker_count,
501 });
502 }
503 Ok(())
504}
505
506#[cfg(test)]
507mod tests;