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