Skip to main content

eredu_core/residency/
prefetch.rs

1//! Backend-neutral admission and lifecycle for bounded residency prefetching.
2
3use std::{
4    collections::{BTreeMap, BTreeSet, VecDeque},
5    time::Duration,
6};
7
8use serde::{Deserialize, Serialize};
9
10use super::OffloadUnitId;
11
12/// Immutable observations from one bounded background prefetch executor.
13#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
14pub struct BackgroundPrefetchReport {
15    submitted: u64,
16    coalesced: u64,
17    started: u64,
18    completed: u64,
19    cancelled: u64,
20    failed: u64,
21    queue_capacity: usize,
22    peak_queue_occupancy: usize,
23    backpressure_count: u64,
24    backpressure_duration: Duration,
25    demand_waits: u64,
26    demand_wait_duration: Duration,
27    ready_before_demand: u64,
28    in_flight_at_demand: u64,
29    evicted_before_use: u64,
30}
31
32impl BackgroundPrefetchReport {
33    /// Requests admitted for background execution.
34    pub const fn submitted(self) -> u64 {
35        self.submitted
36    }
37
38    /// Duplicate or already-resident requests folded into existing work.
39    pub const fn coalesced(self) -> u64 {
40        self.coalesced
41    }
42
43    /// Requests handed to a backend executor.
44    pub const fn started(self) -> u64 {
45        self.started
46    }
47
48    /// Requests published successfully.
49    pub const fn completed(self) -> u64 {
50        self.completed
51    }
52
53    /// Queued or submitted requests discarded by cancellation.
54    pub const fn cancelled(self) -> u64 {
55        self.cancelled
56    }
57
58    /// Backend operations whose failures were retained for demand.
59    pub const fn failed(self) -> u64 {
60        self.failed
61    }
62
63    /// Maximum number of admitted operations awaiting execution.
64    pub const fn queue_capacity(self) -> usize {
65        self.queue_capacity
66    }
67
68    /// Largest observed admitted queue occupancy.
69    pub const fn peak_queue_occupancy(self) -> usize {
70        self.peak_queue_occupancy
71    }
72
73    /// Submissions that encountered full admission capacity.
74    pub const fn backpressure_count(self) -> u64 {
75        self.backpressure_count
76    }
77
78    /// Cumulative wait time for resolved backpressure events.
79    pub const fn backpressure_duration(self) -> Duration {
80        self.backpressure_duration
81    }
82
83    /// Demand acquisitions that waited for admitted or submitted work.
84    pub const fn demand_waits(self) -> u64 {
85        self.demand_waits
86    }
87
88    /// Time spent waiting for demanded work.
89    pub const fn demand_wait_duration(self) -> Duration {
90        self.demand_wait_duration
91    }
92
93    /// Completed prefetches consumed by demand before eviction.
94    pub const fn ready_before_demand(self) -> u64 {
95        self.ready_before_demand
96    }
97
98    /// Prefetches already submitted to the backend when first demanded.
99    pub const fn in_flight_at_demand(self) -> u64 {
100        self.in_flight_at_demand
101    }
102
103    /// Completed prefetches found evicted before first demand.
104    pub const fn evicted_before_use(self) -> u64 {
105        self.evicted_before_use
106    }
107}
108
109/// Exact logical operation admitted by [`PrefetchExecutionState`].
110#[derive(Debug, Clone, Eq, PartialEq)]
111pub struct PrefetchWork {
112    generation: u64,
113    id: OffloadUnitId,
114}
115
116impl PrefetchWork {
117    /// Cancellation generation in which this work was admitted.
118    pub const fn generation(&self) -> u64 {
119        self.generation
120    }
121
122    /// Logical residency unit to materialize.
123    pub const fn id(&self) -> &OffloadUnitId {
124        &self.id
125    }
126}
127
128/// Result of attempting to admit one prefetch operation.
129#[derive(Debug, Clone, Eq, PartialEq)]
130pub enum PrefetchAdmission {
131    /// A new operation was admitted and awaits backend execution.
132    Admitted(PrefetchWork),
133    /// Existing queued, submitted, completed, or resident state satisfies it.
134    Coalesced,
135    /// The bounded queue cannot admit another operation yet.
136    AtCapacity,
137}
138
139/// State observed when demand first asks for one logical unit.
140#[derive(Debug, Clone, Copy, Eq, PartialEq)]
141pub enum PrefetchDemandObservation {
142    /// The operation is admitted but has not been submitted to the backend.
143    Queued,
144    /// The backend operation is in flight.
145    InFlight,
146    /// A successful prefetch is ready to consume.
147    Ready,
148    /// A backend failure is retained for demand.
149    Failed,
150    /// No background operation owns the unit.
151    Unscheduled,
152}
153
154impl PrefetchDemandObservation {
155    /// Whether demand must wait for a terminal transition.
156    pub const fn is_pending(self) -> bool {
157        matches!(self, Self::Queued | Self::InFlight)
158    }
159}
160
161/// Terminal result consumed by a demand acquisition.
162#[derive(Debug, Clone, Eq, PartialEq)]
163pub enum PrefetchDemandResolution<E> {
164    /// Background execution completed and remained available.
165    Ready,
166    /// Backend execution failed with its structured backend error.
167    Failed(E),
168    /// Demand must perform or acquire the work directly.
169    Unscheduled,
170}
171
172/// Publication disposition after an exact backend operation completes.
173#[derive(Debug, Clone, Copy, Eq, PartialEq)]
174pub enum PrefetchCompletion {
175    /// The completed copy became available to demand.
176    Published,
177    /// The backend failure was retained for demand.
178    Failed,
179    /// Cancellation made this exact completion stale.
180    Discarded,
181}
182
183/// Backend-neutral bounded prefetch admission and execution lifecycle.
184///
185/// The backend owns workers, buffers, I/O, and completion primitives. This
186/// value is the sole owner of FIFO ordering, bounded admission, duplicate
187/// coalescing, exact operation generations, cancellation fencing, failure
188/// retention and recovery, and lifecycle telemetry.
189#[derive(Debug)]
190pub struct PrefetchExecutionState<E> {
191    generation: u64,
192    queue_capacity: usize,
193    queue: VecDeque<PrefetchWork>,
194    queued: BTreeSet<OffloadUnitId>,
195    in_flight: BTreeMap<OffloadUnitId, u64>,
196    completed: BTreeSet<OffloadUnitId>,
197    failures: BTreeMap<OffloadUnitId, E>,
198    report: BackgroundPrefetchReport,
199}
200
201impl<E> PrefetchExecutionState<E> {
202    /// Creates an empty executor state with a finite, nonzero queue capacity.
203    pub fn new(queue_capacity: usize) -> Result<Self, PrefetchStateError> {
204        if queue_capacity == 0 {
205            return Err(PrefetchStateError::ZeroQueueCapacity);
206        }
207        Ok(Self {
208            generation: 0,
209            queue_capacity,
210            queue: VecDeque::new(),
211            queued: BTreeSet::new(),
212            in_flight: BTreeMap::new(),
213            completed: BTreeSet::new(),
214            failures: BTreeMap::new(),
215            report: BackgroundPrefetchReport {
216                queue_capacity,
217                ..BackgroundPrefetchReport::default()
218            },
219        })
220    }
221
222    /// Admits a missing unit, coalesces existing work, or reports backpressure.
223    ///
224    /// `resident` is supplied by the backend after checking its concrete
225    /// storage. A completed logical result that is no longer resident is
226    /// counted as evicted-before-use and is eligible for a new attempt.
227    pub fn admit(&mut self, id: OffloadUnitId, resident: bool) -> PrefetchAdmission {
228        if self.queued.contains(&id) || self.in_flight.contains_key(&id) {
229            self.report.coalesced = self.report.coalesced.saturating_add(1);
230            return PrefetchAdmission::Coalesced;
231        }
232
233        if self.completed.contains(&id) && !resident {
234            self.completed.remove(&id);
235            self.report.evicted_before_use = self.report.evicted_before_use.saturating_add(1);
236        }
237        if resident {
238            self.failures.remove(&id);
239            self.completed.insert(id);
240            self.report.coalesced = self.report.coalesced.saturating_add(1);
241            return PrefetchAdmission::Coalesced;
242        }
243        if self.queue.len() == self.queue_capacity {
244            return PrefetchAdmission::AtCapacity;
245        }
246
247        // A new explicit attempt supersedes a previously observed backend
248        // failure. Demand will see the result of this exact generation.
249        self.failures.remove(&id);
250        let work = PrefetchWork {
251            generation: self.generation,
252            id,
253        };
254        self.queued.insert(work.id.clone());
255        self.queue.push_back(work.clone());
256        self.report.submitted = self.report.submitted.saturating_add(1);
257        self.report.peak_queue_occupancy = self.report.peak_queue_occupancy.max(self.queue.len());
258        PrefetchAdmission::Admitted(work)
259    }
260
261    /// Rolls back one admitted operation whose backend notification failed.
262    pub fn rollback_admission(&mut self, work: &PrefetchWork) -> Result<(), PrefetchStateError> {
263        let Some(position) = self.queue.iter().position(|queued| queued == work) else {
264            return Err(PrefetchStateError::WorkNotQueued {
265                id: work.id.clone(),
266                generation: work.generation,
267            });
268        };
269        self.queue.remove(position);
270        self.queued.remove(&work.id);
271        self.report.submitted = self.report.submitted.saturating_sub(1);
272        Ok(())
273    }
274
275    /// Selects the oldest admitted operation for backend submission.
276    pub fn begin_next(&mut self) -> Option<PrefetchWork> {
277        let work = self.queue.pop_front()?;
278        self.queued.remove(&work.id);
279        self.in_flight.insert(work.id.clone(), work.generation);
280        self.report.started = self.report.started.saturating_add(1);
281        Some(work)
282    }
283
284    /// Applies one exact backend completion.
285    pub fn complete(
286        &mut self,
287        work: PrefetchWork,
288        result: Result<(), E>,
289    ) -> Result<PrefetchCompletion, PrefetchStateError> {
290        let Some(active_generation) = self.in_flight.get(&work.id).copied() else {
291            return Err(PrefetchStateError::WorkNotInFlight {
292                id: work.id,
293                generation: work.generation,
294            });
295        };
296        if active_generation != work.generation {
297            return Err(PrefetchStateError::CompletionGenerationMismatch {
298                id: work.id,
299                expected: active_generation,
300                actual: work.generation,
301            });
302        }
303        self.in_flight.remove(&work.id);
304        if work.generation != self.generation {
305            self.report.cancelled = self.report.cancelled.saturating_add(1);
306            return Ok(PrefetchCompletion::Discarded);
307        }
308        match result {
309            Ok(()) => {
310                self.completed.insert(work.id);
311                self.report.completed = self.report.completed.saturating_add(1);
312                Ok(PrefetchCompletion::Published)
313            }
314            Err(error) => {
315                self.failures.insert(work.id, error);
316                self.report.failed = self.report.failed.saturating_add(1);
317                Ok(PrefetchCompletion::Failed)
318            }
319        }
320    }
321
322    /// Observes current background ownership when demand first arrives.
323    pub fn observe_demand(&mut self, id: &OffloadUnitId) -> PrefetchDemandObservation {
324        if self.queued.contains(id) {
325            PrefetchDemandObservation::Queued
326        } else if self.in_flight.contains_key(id) {
327            self.report.in_flight_at_demand = self.report.in_flight_at_demand.saturating_add(1);
328            PrefetchDemandObservation::InFlight
329        } else if self.failures.contains_key(id) {
330            PrefetchDemandObservation::Failed
331        } else if self.completed.contains(id) {
332            PrefetchDemandObservation::Ready
333        } else {
334            PrefetchDemandObservation::Unscheduled
335        }
336    }
337
338    /// Whether an admitted or submitted operation still owns this unit.
339    pub fn is_pending(&self, id: &OffloadUnitId) -> bool {
340        self.queued.contains(id) || self.in_flight.contains_key(id)
341    }
342
343    /// Consumes the terminal background result for one demand acquisition.
344    pub fn resolve_demand(
345        &mut self,
346        id: &OffloadUnitId,
347        waited: Option<Duration>,
348    ) -> Result<PrefetchDemandResolution<E>, PrefetchStateError> {
349        if self.is_pending(id) {
350            return Err(PrefetchStateError::DemandStillPending { id: id.clone() });
351        }
352        if let Some(duration) = waited {
353            self.report.demand_waits = self.report.demand_waits.saturating_add(1);
354            self.report.demand_wait_duration =
355                self.report.demand_wait_duration.saturating_add(duration);
356        }
357        if let Some(error) = self.failures.remove(id) {
358            return Ok(PrefetchDemandResolution::Failed(error));
359        }
360        if self.completed.remove(id) {
361            self.report.ready_before_demand = self.report.ready_before_demand.saturating_add(1);
362            return Ok(PrefetchDemandResolution::Ready);
363        }
364        Ok(PrefetchDemandResolution::Unscheduled)
365    }
366
367    /// Records a submission when it first encounters bounded admission capacity.
368    pub fn begin_backpressure(&mut self) {
369        self.report.backpressure_count = self.report.backpressure_count.saturating_add(1);
370    }
371
372    /// Records how long one backpressured submission waited before resolving.
373    pub fn finish_backpressure(&mut self, duration: Duration) {
374        self.report.backpressure_duration =
375            self.report.backpressure_duration.saturating_add(duration);
376    }
377
378    /// Cancels all queued work immediately and fences exact in-flight work.
379    ///
380    /// In-flight backend resources remain owned until [`Self::complete`] sees
381    /// their exact operation. Queued work needs no backend cancellation and is
382    /// discarded synchronously.
383    pub fn cancel_all(&mut self) -> Result<(), PrefetchStateError> {
384        self.generation = self
385            .generation
386            .checked_add(1)
387            .ok_or(PrefetchStateError::GenerationExhausted)?;
388        self.report.cancelled = self
389            .report
390            .cancelled
391            .saturating_add(self.queue.len() as u64);
392        self.queue.clear();
393        self.queued.clear();
394        Ok(())
395    }
396
397    /// Finishes cancellation after every exact in-flight operation resolved.
398    ///
399    /// Completed prefetches are abandoned. The first retained backend failure
400    /// is returned in deterministic logical-unit order.
401    pub fn finish_cancellation(
402        &mut self,
403    ) -> Result<Option<(OffloadUnitId, E)>, PrefetchStateError> {
404        if !self.is_idle() {
405            return Err(PrefetchStateError::CancellationStillInFlight);
406        }
407        self.completed.clear();
408        let failure = self.failures.pop_first();
409        self.failures.clear();
410        Ok(failure)
411    }
412
413    /// Whether no admitted or backend-submitted operation remains.
414    pub fn is_idle(&self) -> bool {
415        self.queue.is_empty() && self.in_flight.is_empty()
416    }
417
418    /// Current cancellation generation.
419    pub const fn generation(&self) -> u64 {
420        self.generation
421    }
422
423    /// Immutable telemetry snapshot.
424    pub const fn report(&self) -> BackgroundPrefetchReport {
425        self.report
426    }
427}
428
429/// Invalid use of the prefetch execution lifecycle.
430#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
431pub enum PrefetchStateError {
432    /// Bounded admission requires at least one queue slot.
433    #[error("background prefetch queue capacity must be nonzero")]
434    ZeroQueueCapacity,
435    /// The monotonic cancellation generation cannot advance safely.
436    #[error("background prefetch cancellation generation exhausted")]
437    GenerationExhausted,
438    /// Backend notification rollback did not refer to admitted queued work.
439    #[error("prefetch work {id} generation {generation} is not queued")]
440    WorkNotQueued {
441        /// Logical unit in the invalid operation.
442        id: OffloadUnitId,
443        /// Exact operation generation.
444        generation: u64,
445    },
446    /// Completion did not refer to backend-submitted work.
447    #[error("prefetch work {id} generation {generation} is not in flight")]
448    WorkNotInFlight {
449        /// Logical unit in the invalid operation.
450        id: OffloadUnitId,
451        /// Exact operation generation.
452        generation: u64,
453    },
454    /// Completion used a different exact generation from the active operation.
455    #[error("prefetch completion generation mismatch for {id}: expected {expected}, got {actual}")]
456    CompletionGenerationMismatch {
457        /// Logical unit in the invalid operation.
458        id: OffloadUnitId,
459        /// Generation owned by the in-flight operation.
460        expected: u64,
461        /// Generation supplied by the completion.
462        actual: u64,
463    },
464    /// Demand attempted to consume a nonterminal operation.
465    #[error("prefetch demand for {id} is still pending")]
466    DemandStillPending {
467        /// Logical unit whose operation is nonterminal.
468        id: OffloadUnitId,
469    },
470    /// Cancellation finalization preceded exact in-flight completion.
471    #[error("background prefetch cancellation still owns in-flight work")]
472    CancellationStillInFlight,
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    fn id(value: &str) -> OffloadUnitId {
480        OffloadUnitId::new(value).unwrap()
481    }
482
483    #[derive(Debug, Default)]
484    struct MockBackend {
485        executed: Vec<OffloadUnitId>,
486    }
487
488    impl MockBackend {
489        fn execute(
490            &mut self,
491            state: &mut PrefetchExecutionState<&'static str>,
492            result: Result<(), &'static str>,
493        ) -> PrefetchCompletion {
494            let work = state.begin_next().expect("mock backend has admitted work");
495            self.executed.push(work.id().clone());
496            state.complete(work, result).unwrap()
497        }
498    }
499
500    #[test]
501    fn mock_backend_reuses_fifo_admission_coalescing_and_exact_completion() {
502        let mut state = PrefetchExecutionState::new(2).unwrap();
503        let first = id("layer.0");
504        let second = id("layer.1");
505        let third = id("layer.2");
506
507        assert!(matches!(
508            state.admit(first.clone(), false),
509            PrefetchAdmission::Admitted(_)
510        ));
511        assert_eq!(
512            state.admit(first.clone(), false),
513            PrefetchAdmission::Coalesced
514        );
515        assert!(matches!(
516            state.admit(second.clone(), false),
517            PrefetchAdmission::Admitted(_)
518        ));
519        assert_eq!(
520            state.admit(third.clone(), false),
521            PrefetchAdmission::AtCapacity
522        );
523
524        let mut backend = MockBackend::default();
525        assert_eq!(
526            backend.execute(&mut state, Ok(())),
527            PrefetchCompletion::Published
528        );
529        assert!(matches!(
530            state.admit(third.clone(), false),
531            PrefetchAdmission::Admitted(_)
532        ));
533        backend.execute(&mut state, Ok(()));
534        backend.execute(&mut state, Ok(()));
535        assert_eq!(backend.executed, [first, second, third]);
536        assert_eq!(state.report().submitted(), 3);
537        assert_eq!(state.report().coalesced(), 1);
538        assert_eq!(state.report().peak_queue_occupancy(), 2);
539    }
540
541    #[test]
542    fn cancellation_discards_queue_but_retains_exact_in_flight_ownership() {
543        let mut state = PrefetchExecutionState::<()>::new(2).unwrap();
544        let active = id("layer.0");
545        let queued = id("layer.1");
546        state.admit(active.clone(), false);
547        state.admit(queued, false);
548        let work = state.begin_next().unwrap();
549
550        state.cancel_all().unwrap();
551        assert!(!state.is_idle());
552        assert!(matches!(
553            state.finish_cancellation(),
554            Err(PrefetchStateError::CancellationStillInFlight)
555        ));
556        assert_eq!(
557            state.complete(work, Ok(())).unwrap(),
558            PrefetchCompletion::Discarded
559        );
560        assert!(state.is_idle());
561        assert_eq!(state.finish_cancellation().unwrap(), None);
562        assert_eq!(state.report().cancelled(), 2);
563        assert_eq!(state.report().completed(), 0);
564        assert_eq!(
565            state.observe_demand(&active),
566            PrefetchDemandObservation::Unscheduled
567        );
568    }
569
570    #[test]
571    fn failure_is_delivered_once_and_a_new_attempt_supersedes_it() {
572        let mut state = PrefetchExecutionState::new(1).unwrap();
573        let unit = id("layer.0");
574        state.admit(unit.clone(), false);
575        let work = state.begin_next().unwrap();
576        state.complete(work, Err("disk read failed")).unwrap();
577        assert_eq!(
578            state.observe_demand(&unit),
579            PrefetchDemandObservation::Failed
580        );
581
582        assert!(matches!(
583            state.admit(unit.clone(), false),
584            PrefetchAdmission::Admitted(_)
585        ));
586        assert_eq!(
587            state.observe_demand(&unit),
588            PrefetchDemandObservation::Queued
589        );
590        let work = state.begin_next().unwrap();
591        state.complete(work, Ok(())).unwrap();
592        assert_eq!(
593            state
594                .resolve_demand(&unit, Some(Duration::from_millis(3)))
595                .unwrap(),
596            PrefetchDemandResolution::Ready
597        );
598        assert_eq!(state.report().failed(), 1);
599        assert_eq!(state.report().completed(), 1);
600        assert_eq!(state.report().demand_waits(), 1);
601    }
602
603    #[test]
604    fn rollback_and_residency_observation_preserve_admission_accounting() {
605        let mut state = PrefetchExecutionState::<()>::new(1).unwrap();
606        let unit = id("layer.0");
607        let PrefetchAdmission::Admitted(work) = state.admit(unit.clone(), false) else {
608            panic!("missing unit should be admitted");
609        };
610        state.rollback_admission(&work).unwrap();
611        assert_eq!(state.report().submitted(), 0);
612        assert!(state.is_idle());
613
614        assert_eq!(
615            state.admit(unit.clone(), true),
616            PrefetchAdmission::Coalesced
617        );
618        assert_eq!(
619            state.observe_demand(&unit),
620            PrefetchDemandObservation::Ready
621        );
622        assert_eq!(
623            state.resolve_demand(&unit, None).unwrap(),
624            PrefetchDemandResolution::Ready
625        );
626        assert_eq!(state.report().ready_before_demand(), 1);
627    }
628
629    #[test]
630    fn report_serialization_round_trip_preserves_stable_fields() {
631        let mut state = PrefetchExecutionState::<()>::new(3).unwrap();
632        state.begin_backpressure();
633        assert_eq!(state.report().backpressure_count(), 1);
634        assert_eq!(state.report().backpressure_duration(), Duration::ZERO);
635        state.finish_backpressure(Duration::from_millis(7));
636        state.admit(id("layer.0"), false);
637        let report = state.report();
638        let encoded = serde_json::to_string(&report).unwrap();
639        let decoded: BackgroundPrefetchReport = serde_json::from_str(&encoded).unwrap();
640        assert_eq!(decoded, report);
641        assert_eq!(decoded.queue_capacity(), 3);
642        assert_eq!(decoded.backpressure_count(), 1);
643        assert_eq!(decoded.backpressure_duration(), Duration::from_millis(7));
644    }
645}