Skip to main content

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;