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;