tenferro_cpu/domain_executor.rs
1use std::fmt::Debug;
2use std::num::NonZeroUsize;
3use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
4use std::sync::Arc;
5
6use rayon::prelude::*;
7
8/// Inner parallel-region support offered by a CPU domain executor.
9///
10/// # Examples
11///
12/// ```rust
13/// use tenferro_cpu::CpuInnerParallelism;
14///
15/// assert_ne!(CpuInnerParallelism::None, CpuInnerParallelism::Rayon);
16/// ```
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum CpuInnerParallelism {
19 /// The executor cannot host provider-owned inner parallel regions.
20 None,
21 /// The executor can host a Rayon-compatible inner parallel region.
22 Rayon,
23}
24
25/// Re-entry capability of one CPU domain executor.
26///
27/// This describes executor-level same-executor entry only. It never grants
28/// permission for recursive public [`crate::CpuBackend`] entry, which remains a
29/// separate backend contract.
30///
31/// # Examples
32///
33/// ```rust
34/// use tenferro_cpu::CpuExecutorReentrancy;
35///
36/// let policy = CpuExecutorReentrancy::Rejected;
37/// assert_eq!(policy, CpuExecutorReentrancy::Rejected);
38/// ```
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum CpuExecutorReentrancy {
41 /// Nested entry into the same executor is rejected.
42 Rejected,
43 /// The executor supports nested entry into that same executor.
44 SameExecutor,
45}
46
47/// Affinity claim made by a CPU domain executor.
48///
49/// # Examples
50///
51/// ```rust
52/// use tenferro_cpu::CpuExecutorAffinity;
53///
54/// let affinity = CpuExecutorAffinity::CallerDeclaredUnverified;
55/// assert_ne!(affinity, CpuExecutorAffinity::TenferroPinnedVerified);
56/// ```
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum CpuExecutorAffinity {
59 /// Tenferro pinned the workers and verified their placement.
60 TenferroPinnedVerified,
61 /// The caller declared worker placement, but tenferro did not verify it.
62 CallerDeclaredUnverified,
63 /// The executor makes no worker-placement claim.
64 None,
65}
66
67/// Ownership of CPU executor shutdown.
68///
69/// # Examples
70///
71/// ```rust
72/// use tenferro_cpu::CpuExecutorShutdown;
73///
74/// assert_ne!(
75/// CpuExecutorShutdown::TenferroOwned,
76/// CpuExecutorShutdown::CallerOwned,
77/// );
78/// ```
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub enum CpuExecutorShutdown {
81 /// Tenferro owns executor shutdown.
82 TenferroOwned,
83 /// The caller owns executor shutdown and executor lifetime policy.
84 CallerOwned,
85}
86
87/// Immutable construction-time capabilities of a CPU domain executor.
88///
89/// # Examples
90///
91/// ```rust
92/// use std::num::NonZeroUsize;
93/// use tenferro_cpu::{
94/// CpuDomainExecutorCapabilities, CpuExecutorAffinity, CpuExecutorReentrancy,
95/// CpuExecutorShutdown, CpuInnerParallelism,
96/// };
97///
98/// let capabilities = CpuDomainExecutorCapabilities {
99/// worker_count: NonZeroUsize::new(4).unwrap(),
100/// outer_parallelism: true,
101/// inner_parallelism: CpuInnerParallelism::Rayon,
102/// reentrancy: CpuExecutorReentrancy::Rejected,
103/// affinity: CpuExecutorAffinity::TenferroPinnedVerified,
104/// shutdown: CpuExecutorShutdown::TenferroOwned,
105/// };
106/// assert_eq!(capabilities.worker_count.get(), 4);
107/// ```
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub struct CpuDomainExecutorCapabilities {
110 /// Number of workers made available to this domain.
111 pub worker_count: NonZeroUsize,
112 /// Whether indexed outer fork/join submission is supported.
113 pub outer_parallelism: bool,
114 /// Provider-owned inner parallel-region support.
115 pub inner_parallelism: CpuInnerParallelism,
116 /// Same-executor re-entry capability.
117 pub reentrancy: CpuExecutorReentrancy,
118 /// Worker-affinity claim and verification level.
119 pub affinity: CpuExecutorAffinity,
120 /// Executor shutdown owner.
121 pub shutdown: CpuExecutorShutdown,
122}
123
124/// Failure at the CPU executor admission or scheduling boundary.
125///
126/// Operation and provider errors do not belong in this type. Executors use
127/// these variants only for their own admission, scheduling, cancellation, and
128/// panic-bridge failures.
129///
130/// # Examples
131///
132/// ```rust
133/// use tenferro_cpu::CpuDomainExecutorError;
134///
135/// let error = CpuDomainExecutorError::Admission {
136/// message: "domain is busy".to_string(),
137/// };
138/// assert!(matches!(error, CpuDomainExecutorError::Admission { .. }));
139/// ```
140#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
141pub enum CpuDomainExecutorError {
142 /// The executor rejected entry before scheduling work.
143 #[error("CPU domain executor admission failed: {message}")]
144 Admission {
145 /// Executor-owned diagnostic.
146 message: String,
147 },
148 /// The executor could not schedule or complete submitted work.
149 #[error("CPU domain executor scheduling failed: {message}")]
150 Scheduling {
151 /// Executor-owned diagnostic.
152 message: String,
153 },
154 /// The executor cancelled submitted work.
155 #[error("CPU domain executor cancelled work: {message}")]
156 Cancellation {
157 /// Executor-owned diagnostic.
158 message: String,
159 },
160 /// The executor converted a worker panic into a typed failure.
161 #[error("CPU domain executor worker panicked: {message}")]
162 PanicBridge {
163 /// Executor-owned diagnostic.
164 message: String,
165 },
166}
167
168/// One borrowed job installed synchronously into a CPU domain executor.
169///
170/// The executor must finish using the job before [`CpuDomainExecutor::install`]
171/// returns; a borrowed job never escapes that call.
172///
173/// # Examples
174///
175/// ```rust
176/// use tenferro_cpu::{CpuDomainExecutorError, ScopedCpuJob};
177///
178/// struct Job(bool);
179/// impl ScopedCpuJob for Job {
180/// fn run(&mut self) -> Result<(), CpuDomainExecutorError> {
181/// self.0 = true;
182/// Ok(())
183/// }
184/// }
185/// let mut job = Job(false);
186/// job.run().unwrap();
187/// assert!(job.0);
188/// ```
189pub trait ScopedCpuJob: Send {
190 /// Run this job once on the executor-selected calling context.
191 ///
192 /// # Examples
193 ///
194 /// ```rust
195 /// use tenferro_cpu::{CpuDomainExecutorError, ScopedCpuJob};
196 ///
197 /// struct Job;
198 /// impl ScopedCpuJob for Job {
199 /// fn run(&mut self) -> Result<(), CpuDomainExecutorError> { Ok(()) }
200 /// }
201 /// assert!(Job.run().is_ok());
202 /// ```
203 ///
204 /// # Errors
205 ///
206 /// Returns [`CpuDomainExecutorError::Admission`],
207 /// [`CpuDomainExecutorError::Scheduling`],
208 /// [`CpuDomainExecutorError::Cancellation`], or
209 /// [`CpuDomainExecutorError::PanicBridge`] when that failure is observed
210 /// while running the job.
211 fn run(&mut self) -> Result<(), CpuDomainExecutorError>;
212}
213
214/// Synchronously submitted indexed jobs for engine-owned outer scheduling.
215///
216/// Implementations expose a borrowed logical range `0..len()` without
217/// allocating a job collection. Every indexed call must finish before
218/// [`CpuDomainExecutor::submit`] returns.
219///
220/// # Examples
221///
222/// ```rust
223/// use tenferro_cpu::{CpuDomainExecutorError, ScopedCpuJobs};
224///
225/// struct Jobs;
226/// impl ScopedCpuJobs for Jobs {
227/// fn len(&self) -> usize { 2 }
228/// fn run(&self, index: usize) -> Result<(), CpuDomainExecutorError> {
229/// assert!(index < self.len());
230/// Ok(())
231/// }
232/// }
233/// let jobs: &dyn ScopedCpuJobs = &Jobs;
234/// assert_eq!(jobs.len(), 2);
235/// jobs.run(1).unwrap();
236/// ```
237pub trait ScopedCpuJobs: Sync {
238 /// Return the number of indexed jobs in this synchronous submission.
239 ///
240 /// # Examples
241 ///
242 /// ```rust
243 /// use tenferro_cpu::{CpuDomainExecutorError, ScopedCpuJobs};
244 ///
245 /// struct Jobs;
246 /// impl ScopedCpuJobs for Jobs {
247 /// fn len(&self) -> usize { 3 }
248 /// fn run(&self, _index: usize) -> Result<(), CpuDomainExecutorError> { Ok(()) }
249 /// }
250 /// assert_eq!(Jobs.len(), 3);
251 /// ```
252 fn len(&self) -> usize;
253
254 /// Return whether this submission contains no indexed jobs.
255 ///
256 /// # Examples
257 ///
258 /// ```rust
259 /// use tenferro_cpu::{CpuDomainExecutorError, ScopedCpuJobs};
260 ///
261 /// struct Jobs;
262 /// impl ScopedCpuJobs for Jobs {
263 /// fn len(&self) -> usize { 0 }
264 /// fn run(&self, _index: usize) -> Result<(), CpuDomainExecutorError> { Ok(()) }
265 /// }
266 /// assert!(Jobs.is_empty());
267 /// ```
268 fn is_empty(&self) -> bool {
269 self.len() == 0
270 }
271
272 /// Run one indexed job synchronously.
273 ///
274 /// # Examples
275 ///
276 /// ```rust
277 /// use tenferro_cpu::{CpuDomainExecutorError, ScopedCpuJobs};
278 ///
279 /// struct Jobs;
280 /// impl ScopedCpuJobs for Jobs {
281 /// fn len(&self) -> usize { 1 }
282 /// fn run(&self, index: usize) -> Result<(), CpuDomainExecutorError> {
283 /// assert_eq!(index, 0);
284 /// Ok(())
285 /// }
286 /// }
287 /// Jobs.run(0).unwrap();
288 /// ```
289 ///
290 /// # Errors
291 ///
292 /// Returns [`CpuDomainExecutorError::Admission`],
293 /// [`CpuDomainExecutorError::Scheduling`],
294 /// [`CpuDomainExecutorError::Cancellation`], or
295 /// [`CpuDomainExecutorError::PanicBridge`] when that failure is observed
296 /// while running the indexed job.
297 fn run(&self, index: usize) -> Result<(), CpuDomainExecutorError>;
298}
299
300/// Object-safe synchronous executor for one CPU resource domain.
301///
302/// `submit` is an indexed fork/join boundary and `install` is one borrowed
303/// provider-owned inner-region entry. Neither method may retain its borrowed
304/// job after returning.
305///
306/// # Examples
307///
308/// ```rust
309/// use std::num::NonZeroUsize;
310/// use tenferro_cpu::{
311/// CpuDomainExecutor, CpuDomainExecutorCapabilities, CpuDomainExecutorError,
312/// CpuExecutorAffinity, CpuExecutorReentrancy, CpuExecutorShutdown,
313/// CpuInnerParallelism, ScopedCpuJob, ScopedCpuJobs,
314/// };
315///
316/// #[derive(Debug)]
317/// struct Inline;
318/// impl CpuDomainExecutor for Inline {
319/// fn capabilities(&self) -> CpuDomainExecutorCapabilities {
320/// CpuDomainExecutorCapabilities {
321/// worker_count: NonZeroUsize::new(1).unwrap(),
322/// outer_parallelism: false,
323/// inner_parallelism: CpuInnerParallelism::None,
324/// reentrancy: CpuExecutorReentrancy::Rejected,
325/// affinity: CpuExecutorAffinity::None,
326/// shutdown: CpuExecutorShutdown::CallerOwned,
327/// }
328/// }
329/// fn submit(&self, _jobs: &dyn ScopedCpuJobs) -> Result<(), CpuDomainExecutorError> {
330/// Ok(())
331/// }
332/// fn install(&self, job: &mut dyn ScopedCpuJob) -> Result<(), CpuDomainExecutorError> {
333/// job.run()
334/// }
335/// }
336/// let executor: &dyn CpuDomainExecutor = &Inline;
337/// assert_eq!(executor.capabilities().worker_count.get(), 1);
338/// ```
339pub trait CpuDomainExecutor: Debug + Send + Sync + 'static {
340 /// Return immutable construction-time executor capabilities.
341 ///
342 /// # Examples
343 ///
344 /// ```rust
345 /// use std::num::NonZeroUsize;
346 /// use tenferro_cpu::{
347 /// CpuDomainExecutor, CpuDomainExecutorCapabilities, CpuDomainExecutorError,
348 /// CpuExecutorAffinity, CpuExecutorReentrancy, CpuExecutorShutdown,
349 /// CpuInnerParallelism, ScopedCpuJob, ScopedCpuJobs,
350 /// };
351 /// # #[derive(Debug)] struct Inline;
352 /// # impl CpuDomainExecutor for Inline {
353 /// # fn capabilities(&self) -> CpuDomainExecutorCapabilities {
354 /// # CpuDomainExecutorCapabilities { worker_count: NonZeroUsize::new(2).unwrap(),
355 /// # outer_parallelism: true, inner_parallelism: CpuInnerParallelism::None,
356 /// # reentrancy: CpuExecutorReentrancy::Rejected, affinity: CpuExecutorAffinity::None,
357 /// # shutdown: CpuExecutorShutdown::CallerOwned }
358 /// # }
359 /// # fn submit(&self, _jobs: &dyn ScopedCpuJobs) -> Result<(), CpuDomainExecutorError> { Ok(()) }
360 /// # fn install(&self, job: &mut dyn ScopedCpuJob) -> Result<(), CpuDomainExecutorError> { job.run() }
361 /// # }
362 /// let executor: &dyn CpuDomainExecutor = &Inline;
363 /// assert_eq!(executor.capabilities().worker_count.get(), 2);
364 /// ```
365 fn capabilities(&self) -> CpuDomainExecutorCapabilities;
366
367 /// Submit all indexed jobs as one synchronous fork/join operation.
368 ///
369 /// All `0..jobs.len()` jobs must be complete when this method returns.
370 ///
371 /// # Examples
372 ///
373 /// ```rust
374 /// use std::num::NonZeroUsize;
375 /// use std::sync::atomic::{AtomicUsize, Ordering};
376 /// use tenferro_cpu::{
377 /// CpuDomainExecutor, CpuDomainExecutorCapabilities, CpuDomainExecutorError,
378 /// CpuExecutorAffinity, CpuExecutorReentrancy, CpuExecutorShutdown,
379 /// CpuInnerParallelism, ScopedCpuJob, ScopedCpuJobs,
380 /// };
381 /// # #[derive(Debug)] struct Inline;
382 /// # impl CpuDomainExecutor for Inline {
383 /// # fn capabilities(&self) -> CpuDomainExecutorCapabilities {
384 /// # CpuDomainExecutorCapabilities { worker_count: NonZeroUsize::new(1).unwrap(),
385 /// # outer_parallelism: true, inner_parallelism: CpuInnerParallelism::None,
386 /// # reentrancy: CpuExecutorReentrancy::Rejected, affinity: CpuExecutorAffinity::None,
387 /// # shutdown: CpuExecutorShutdown::CallerOwned }
388 /// # }
389 /// # fn submit(&self, jobs: &dyn ScopedCpuJobs) -> Result<(), CpuDomainExecutorError> {
390 /// # for index in 0..jobs.len() { jobs.run(index)?; } Ok(())
391 /// # }
392 /// # fn install(&self, job: &mut dyn ScopedCpuJob) -> Result<(), CpuDomainExecutorError> { job.run() }
393 /// # }
394 /// struct Jobs<'a>(&'a AtomicUsize);
395 /// impl ScopedCpuJobs for Jobs<'_> {
396 /// fn len(&self) -> usize { 2 }
397 /// fn run(&self, _index: usize) -> Result<(), CpuDomainExecutorError> {
398 /// self.0.fetch_add(1, Ordering::Relaxed);
399 /// Ok(())
400 /// }
401 /// }
402 /// let count = AtomicUsize::new(0);
403 /// Inline.submit(&Jobs(&count)).unwrap();
404 /// assert_eq!(count.load(Ordering::Relaxed), 2);
405 /// ```
406 ///
407 /// # Errors
408 ///
409 /// Returns [`CpuDomainExecutorError::Admission`],
410 /// [`CpuDomainExecutorError::Scheduling`],
411 /// [`CpuDomainExecutorError::Cancellation`], or
412 /// [`CpuDomainExecutorError::PanicBridge`] for executor-owned failures.
413 fn submit(&self, jobs: &dyn ScopedCpuJobs) -> Result<(), CpuDomainExecutorError>;
414
415 /// Enter one synchronous provider-owned inner parallel region.
416 ///
417 /// # Examples
418 ///
419 /// ```rust
420 /// use std::num::NonZeroUsize;
421 /// use tenferro_cpu::{
422 /// CpuDomainExecutor, CpuDomainExecutorCapabilities, CpuDomainExecutorError,
423 /// CpuExecutorAffinity, CpuExecutorReentrancy, CpuExecutorShutdown,
424 /// CpuInnerParallelism, ScopedCpuJob, ScopedCpuJobs,
425 /// };
426 /// # #[derive(Debug)] struct Inline;
427 /// # impl CpuDomainExecutor for Inline {
428 /// # fn capabilities(&self) -> CpuDomainExecutorCapabilities {
429 /// # CpuDomainExecutorCapabilities { worker_count: NonZeroUsize::new(1).unwrap(),
430 /// # outer_parallelism: false, inner_parallelism: CpuInnerParallelism::None,
431 /// # reentrancy: CpuExecutorReentrancy::Rejected, affinity: CpuExecutorAffinity::None,
432 /// # shutdown: CpuExecutorShutdown::CallerOwned }
433 /// # }
434 /// # fn submit(&self, _jobs: &dyn ScopedCpuJobs) -> Result<(), CpuDomainExecutorError> { Ok(()) }
435 /// # fn install(&self, job: &mut dyn ScopedCpuJob) -> Result<(), CpuDomainExecutorError> { job.run() }
436 /// # }
437 /// struct Job(bool);
438 /// impl ScopedCpuJob for Job {
439 /// fn run(&mut self) -> Result<(), CpuDomainExecutorError> {
440 /// self.0 = true;
441 /// Ok(())
442 /// }
443 /// }
444 /// let mut job = Job(false);
445 /// Inline.install(&mut job).unwrap();
446 /// assert!(job.0);
447 /// ```
448 ///
449 /// # Errors
450 ///
451 /// Returns [`CpuDomainExecutorError::Admission`],
452 /// [`CpuDomainExecutorError::Scheduling`],
453 /// [`CpuDomainExecutorError::Cancellation`], or
454 /// [`CpuDomainExecutorError::PanicBridge`] for executor-owned failures.
455 fn install(&self, job: &mut dyn ScopedCpuJob) -> Result<(), CpuDomainExecutorError>;
456}
457
458/// Adapter that executes CPU-domain jobs on one caller-owned Rayon pool.
459///
460/// The adapter retains the supplied pool and never creates, reconfigures, or
461/// shuts it down. [`CpuDomainExecutor`] remains the primary injection contract;
462/// this type is only a convenience for Rayon hosts.
463///
464/// # Examples
465///
466/// ```rust
467/// use std::sync::Arc;
468/// use tenferro_cpu::{CpuDomainExecutor, RayonCpuDomainExecutor};
469///
470/// let pool = Arc::new(rayon::ThreadPoolBuilder::new().num_threads(2).build()?);
471/// let executor = RayonCpuDomainExecutor::new(Arc::clone(&pool));
472/// assert_eq!(executor.capabilities().worker_count.get(), 2);
473/// assert_eq!(Arc::strong_count(&pool), 2);
474/// # Ok::<(), rayon::ThreadPoolBuildError>(())
475/// ```
476pub struct RayonCpuDomainExecutor {
477 pool: Arc<rayon::ThreadPool>,
478}
479
480impl std::fmt::Debug for RayonCpuDomainExecutor {
481 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482 formatter
483 .debug_struct("RayonCpuDomainExecutor")
484 .field("worker_count", &self.pool.current_num_threads())
485 .finish_non_exhaustive()
486 }
487}
488
489impl RayonCpuDomainExecutor {
490 /// Retain one caller-owned Rayon pool as a CPU-domain executor.
491 ///
492 /// # Examples
493 ///
494 /// ```rust
495 /// use std::sync::Arc;
496 /// use tenferro_cpu::{CpuDomainExecutor, RayonCpuDomainExecutor};
497 ///
498 /// let pool = Arc::new(rayon::ThreadPoolBuilder::new().num_threads(2).build()?);
499 /// let executor = RayonCpuDomainExecutor::new(pool);
500 /// assert_eq!(executor.capabilities().worker_count.get(), 2);
501 /// # Ok::<(), rayon::ThreadPoolBuildError>(())
502 /// ```
503 pub fn new(pool: Arc<rayon::ThreadPool>) -> Self {
504 Self { pool }
505 }
506}
507
508impl CpuDomainExecutor for RayonCpuDomainExecutor {
509 fn capabilities(&self) -> CpuDomainExecutorCapabilities {
510 // INVARIANT: Rayon rejects thread pools with zero workers.
511 let worker_count =
512 NonZeroUsize::new(self.pool.current_num_threads()).unwrap_or(NonZeroUsize::MIN);
513 CpuDomainExecutorCapabilities {
514 worker_count,
515 outer_parallelism: worker_count.get() > 1,
516 inner_parallelism: CpuInnerParallelism::Rayon,
517 reentrancy: CpuExecutorReentrancy::SameExecutor,
518 affinity: CpuExecutorAffinity::None,
519 shutdown: CpuExecutorShutdown::CallerOwned,
520 }
521 }
522
523 fn submit(&self, jobs: &dyn ScopedCpuJobs) -> Result<(), CpuDomainExecutorError> {
524 self.pool.install(|| {
525 (0..jobs.len())
526 .into_par_iter()
527 .try_for_each(|index| jobs.run(index))
528 })
529 }
530
531 fn install(&self, job: &mut dyn ScopedCpuJob) -> Result<(), CpuDomainExecutorError> {
532 self.pool.install(|| job.run())
533 }
534}
535
536pub(crate) struct ScopedJob<F, R> {
537 operation: Option<F>,
538 result: Option<R>,
539}
540
541pub(crate) fn scoped_job<F, R>(operation: F) -> ScopedJob<F, R>
542where
543 F: FnOnce() -> R + Send,
544 R: Send,
545{
546 ScopedJob {
547 operation: Some(operation),
548 result: None,
549 }
550}
551
552impl<F, R> ScopedJob<F, R> {
553 fn into_result(self) -> Result<R, CpuDomainExecutorError> {
554 self.result
555 .ok_or_else(|| CpuDomainExecutorError::Scheduling {
556 message: "executor returned success without running the scoped CPU job".to_string(),
557 })
558 }
559}
560
561impl<F, R> ScopedCpuJob for ScopedJob<F, R>
562where
563 F: FnOnce() -> R + Send,
564 R: Send,
565{
566 fn run(&mut self) -> Result<(), CpuDomainExecutorError> {
567 let operation =
568 self.operation
569 .take()
570 .ok_or_else(|| CpuDomainExecutorError::Scheduling {
571 message: "executor attempted to run a scoped CPU job more than once"
572 .to_string(),
573 })?;
574 self.result = Some(operation());
575 Ok(())
576 }
577}
578
579pub(crate) fn install_scoped<F, R>(
580 executor: &dyn CpuDomainExecutor,
581 operation: F,
582) -> Result<R, CpuDomainExecutorError>
583where
584 F: FnOnce() -> R + Send,
585 R: Send,
586{
587 let mut job = scoped_job(operation);
588 executor.install(&mut job)?;
589 job.into_result()
590}
591
592pub(crate) struct IndexedJobs<F> {
593 len: usize,
594 run: F,
595 invalid_index_attempt: InvalidIndexAudit,
596}
597
598const INVALID_INDEX_EMPTY: u8 = 0;
599const INVALID_INDEX_WRITING: u8 = 1;
600const INVALID_INDEX_READY: u8 = 2;
601
602// INVARIANT: the two-phase state publishes every usize value, including
603// usize::MAX, without a sentinel collision. Valid `run` calls never touch this
604// audit, and the post-submit Acquire observes the selected invalid index after
605// its Release publication without locking or allocating.
606struct InvalidIndexAudit {
607 state: AtomicU8,
608 index: AtomicUsize,
609}
610
611impl InvalidIndexAudit {
612 const fn new() -> Self {
613 Self {
614 state: AtomicU8::new(INVALID_INDEX_EMPTY),
615 index: AtomicUsize::new(0),
616 }
617 }
618
619 fn record(&self, index: usize) {
620 if self
621 .state
622 .compare_exchange(
623 INVALID_INDEX_EMPTY,
624 INVALID_INDEX_WRITING,
625 Ordering::AcqRel,
626 Ordering::Acquire,
627 )
628 .is_ok()
629 {
630 self.index.store(index, Ordering::Relaxed);
631 self.state.store(INVALID_INDEX_READY, Ordering::Release);
632 }
633 }
634
635 fn load(&self) -> Option<usize> {
636 (self.state.load(Ordering::Acquire) == INVALID_INDEX_READY)
637 .then(|| self.index.load(Ordering::Relaxed))
638 }
639}
640
641pub(crate) fn indexed_jobs<F>(len: usize, run: F) -> IndexedJobs<F>
642where
643 F: Fn(usize) -> Result<(), CpuDomainExecutorError> + Sync,
644{
645 IndexedJobs {
646 len,
647 run,
648 invalid_index_attempt: InvalidIndexAudit::new(),
649 }
650}
651
652impl<F> IndexedJobs<F> {
653 pub(crate) fn invalid_index_attempt(&self) -> Option<usize> {
654 self.invalid_index_attempt.load()
655 }
656}
657
658impl<F> ScopedCpuJobs for IndexedJobs<F>
659where
660 F: Fn(usize) -> Result<(), CpuDomainExecutorError> + Sync,
661{
662 fn len(&self) -> usize {
663 self.len
664 }
665
666 fn run(&self, index: usize) -> Result<(), CpuDomainExecutorError> {
667 if index >= self.len {
668 self.invalid_index_attempt.record(index);
669 return Err(CpuDomainExecutorError::Scheduling {
670 message: format!(
671 "executor requested scoped CPU job index {index}, but the submission has {} jobs",
672 self.len
673 ),
674 });
675 }
676 (self.run)(index)
677 }
678}
679
680#[cfg(test)]
681mod tests;