Skip to main content

eredu_runtime/
prefetch.rs

1//! Backend-neutral bounded background weight prefetch execution.
2
3use std::{
4    panic::{catch_unwind, AssertUnwindSafe},
5    sync::{
6        atomic::{AtomicBool, Ordering},
7        mpsc, Arc, Condvar, Mutex,
8    },
9    thread::{self, JoinHandle},
10    time::Instant,
11};
12
13use eredu_core::residency::{
14    BackgroundPrefetchReport, OffloadUnitId, PrefetchAdmission, PrefetchDemandResolution,
15    PrefetchExecutionState,
16};
17
18enum WorkerMessage {
19    WorkAvailable,
20    Shutdown,
21}
22
23type PrefetchOperation = Arc<dyn Fn(&OffloadUnitId) -> Result<(), String> + Send + Sync + 'static>;
24
25/// One bounded background prefetch worker with exact cancellation and configurable shutdown.
26pub struct BackgroundPrefetchWorker {
27    sender: mpsc::Sender<WorkerMessage>,
28    shared: Arc<(Mutex<PrefetchExecutionState<String>>, Condvar)>,
29    worker: Option<JoinHandle<()>>,
30    nonblocking_drop: bool,
31    stopping: Arc<AtomicBool>,
32}
33
34impl BackgroundPrefetchWorker {
35    /// Starts a named worker which executes one backend-owned operation at a time.
36    pub fn new<F>(
37        capacity: usize,
38        thread_name: impl Into<String>,
39        operation: F,
40    ) -> Result<Self, BackgroundPrefetchWorkerError>
41    where
42        F: Fn(&OffloadUnitId) -> Result<(), String> + Send + Sync + 'static,
43    {
44        let operation: PrefetchOperation = Arc::new(operation);
45        let (sender, receiver) = mpsc::channel();
46        let shared = Arc::new((
47            Mutex::new(PrefetchExecutionState::new(capacity)?),
48            Condvar::new(),
49        ));
50        let worker_shared = Arc::clone(&shared);
51        let stopping = Arc::new(AtomicBool::new(false));
52        let worker_stopping = Arc::clone(&stopping);
53        let worker = thread::Builder::new()
54            .name(thread_name.into())
55            .spawn(move || worker_loop(operation, receiver, worker_shared, worker_stopping))?;
56        Ok(Self {
57            sender,
58            shared,
59            worker: Some(worker),
60            nonblocking_drop: false,
61            stopping,
62        })
63    }
64
65    /// Requests shutdown without joining when this handle is dropped.
66    ///
67    /// The worker retains its operation and lifecycle state until in-flight
68    /// work returns. Queued work is cancelled by that worker; callers needing
69    /// a synchronous fence can still explicitly call [`Self::cancel`].
70    pub fn with_nonblocking_drop(mut self) -> Self {
71        self.nonblocking_drop = true;
72        self
73    }
74
75    /// Admits or coalesces one operation after the backend reports current residency.
76    pub fn submit(
77        &self,
78        id: &OffloadUnitId,
79        resident: bool,
80    ) -> Result<(), BackgroundPrefetchWorkerError> {
81        let mut backpressure_started: Option<Instant> = None;
82        loop {
83            let mut state = self
84                .shared
85                .0
86                .lock()
87                .map_err(|_| BackgroundPrefetchWorkerError::StatePoisoned)?;
88            match state.admit(id.clone(), resident) {
89                PrefetchAdmission::Coalesced => {
90                    if let Some(started) = backpressure_started {
91                        state.finish_backpressure(started.elapsed());
92                    }
93                    return Ok(());
94                }
95                PrefetchAdmission::AtCapacity => {
96                    if backpressure_started.is_none() {
97                        state.begin_backpressure();
98                        backpressure_started = Some(Instant::now());
99                    }
100                    drop(
101                        self.shared
102                            .1
103                            .wait(state)
104                            .map_err(|_| BackgroundPrefetchWorkerError::StatePoisoned)?,
105                    );
106                }
107                PrefetchAdmission::Admitted(work) => {
108                    if let Some(started) = backpressure_started {
109                        state.finish_backpressure(started.elapsed());
110                    }
111                    if self.sender.send(WorkerMessage::WorkAvailable).is_ok() {
112                        return Ok(());
113                    }
114                    state.rollback_admission(&work)?;
115                    self.shared.1.notify_all();
116                    return Err(BackgroundPrefetchWorkerError::WorkerDisconnected);
117                }
118            }
119        }
120    }
121
122    /// Waits for background ownership to resolve and consumes its exact result.
123    pub fn wait(
124        &self,
125        id: &OffloadUnitId,
126    ) -> Result<PrefetchDemandResolution<String>, BackgroundPrefetchWorkerError> {
127        let started = Instant::now();
128        let mut state = self
129            .shared
130            .0
131            .lock()
132            .map_err(|_| BackgroundPrefetchWorkerError::StatePoisoned)?;
133        let waited = state.observe_demand(id).is_pending();
134        while state.is_pending(id) {
135            state = self
136                .shared
137                .1
138                .wait(state)
139                .map_err(|_| BackgroundPrefetchWorkerError::StatePoisoned)?;
140        }
141        Ok(state.resolve_demand(id, waited.then(|| started.elapsed()))?)
142    }
143
144    /// Cancels queued work, fences in-flight work, and returns its first failure.
145    pub fn cancel(&self) -> Result<(), BackgroundPrefetchWorkerError> {
146        let mut state = self
147            .shared
148            .0
149            .lock()
150            .map_err(|_| BackgroundPrefetchWorkerError::StatePoisoned)?;
151        state.cancel_all()?;
152        self.shared.1.notify_all();
153        while !state.is_idle() {
154            state = self
155                .shared
156                .1
157                .wait(state)
158                .map_err(|_| BackgroundPrefetchWorkerError::StatePoisoned)?;
159        }
160        let failure = state.finish_cancellation()?;
161        self.shared.1.notify_all();
162        match failure {
163            Some((id, message)) => {
164                Err(BackgroundPrefetchWorkerError::OperationFailed { id, message })
165            }
166            None => Ok(()),
167        }
168    }
169
170    /// Returns an immutable lifecycle and backpressure report.
171    pub fn report(&self) -> Result<BackgroundPrefetchReport, BackgroundPrefetchWorkerError> {
172        Ok(self
173            .shared
174            .0
175            .lock()
176            .map_err(|_| BackgroundPrefetchWorkerError::StatePoisoned)?
177            .report())
178    }
179
180    /// Waits until all admitted work reaches a terminal state.
181    pub fn wait_idle(&self) -> Result<(), BackgroundPrefetchWorkerError> {
182        let mut state = self
183            .shared
184            .0
185            .lock()
186            .map_err(|_| BackgroundPrefetchWorkerError::StatePoisoned)?;
187        while !state.is_idle() {
188            state = self
189                .shared
190                .1
191                .wait(state)
192                .map_err(|_| BackgroundPrefetchWorkerError::StatePoisoned)?;
193        }
194        Ok(())
195    }
196}
197
198impl Drop for BackgroundPrefetchWorker {
199    fn drop(&mut self) {
200        if self.nonblocking_drop {
201            self.stopping.store(true, Ordering::Release);
202        } else {
203            let _ = self.cancel();
204            let _ = self.sender.send(WorkerMessage::Shutdown);
205        }
206        if let Some(worker) = self.worker.take() {
207            if !self.nonblocking_drop {
208                let _ = worker.join();
209            }
210        }
211    }
212}
213
214fn worker_loop(
215    operation: PrefetchOperation,
216    receiver: mpsc::Receiver<WorkerMessage>,
217    shared: Arc<(Mutex<PrefetchExecutionState<String>>, Condvar)>,
218    stopping: Arc<AtomicBool>,
219) {
220    while let Ok(message) = receiver.recv() {
221        if stopping.load(Ordering::Acquire) {
222            break;
223        }
224        let WorkerMessage::WorkAvailable = message else {
225            break;
226        };
227        let work = {
228            let Ok(mut state) = shared.0.lock() else {
229                break;
230            };
231            let work = state.begin_next();
232            shared.1.notify_all();
233            work
234        };
235        let Some(work) = work else {
236            continue;
237        };
238        let result = catch_unwind(AssertUnwindSafe(|| operation(work.id())))
239            .map_err(|payload| {
240                payload
241                    .downcast_ref::<&str>()
242                    .map(|message| (*message).to_string())
243                    .or_else(|| payload.downcast_ref::<String>().cloned())
244                    .unwrap_or_else(|| "background prefetch operation panicked".to_string())
245            })
246            .and_then(|result| result);
247        let Ok(mut state) = shared.0.lock() else {
248            break;
249        };
250        state
251            .complete(work, result)
252            .expect("worker completion matches runtime-owned admitted work");
253        shared.1.notify_all();
254    }
255    if stopping.load(Ordering::Acquire) {
256        if let Ok(mut state) = shared.0.lock() {
257            let _ = state.cancel_all();
258            let _ = state.finish_cancellation();
259            shared.1.notify_all();
260        }
261    }
262}
263
264/// Failure from the backend-neutral background prefetch worker.
265#[derive(Debug, thiserror::Error)]
266pub enum BackgroundPrefetchWorkerError {
267    /// Shared worker state was poisoned.
268    #[error("background prefetch worker state is poisoned")]
269    StatePoisoned,
270    /// The worker ended before accepting or completing required work.
271    #[error("background prefetch worker disconnected")]
272    WorkerDisconnected,
273    /// A backend operation failed and was retained for demand or cancellation.
274    #[error("background prefetch of {id} failed: {message}")]
275    OperationFailed {
276        /// Failed residency unit.
277        id: OffloadUnitId,
278        /// Original backend failure.
279        message: String,
280    },
281    /// Backend-neutral lifecycle misuse.
282    #[error(transparent)]
283    State(#[from] eredu_core::residency::PrefetchStateError),
284    /// Worker creation failed.
285    #[error(transparent)]
286    Io(#[from] std::io::Error),
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use std::time::Duration;
293
294    fn id(value: &str) -> OffloadUnitId {
295        OffloadUnitId::new(value).unwrap()
296    }
297
298    #[test]
299    fn worker_coalesces_contains_panics_and_reports_demand() {
300        let worker = BackgroundPrefetchWorker::new(2, "runtime-prefetch-test", |id| {
301            if id.as_str() == "panic" {
302                panic!("controlled prefetch panic");
303            }
304            Ok(())
305        })
306        .unwrap();
307        let ready = id("ready");
308        worker.submit(&ready, false).unwrap();
309        worker.submit(&ready, false).unwrap();
310        assert_eq!(
311            worker.wait(&ready).unwrap(),
312            PrefetchDemandResolution::Ready
313        );
314
315        let panic = id("panic");
316        worker.submit(&panic, false).unwrap();
317        let resolution = worker.wait(&panic).unwrap();
318        assert!(
319            matches!(resolution, PrefetchDemandResolution::Failed(message) if message.contains("controlled prefetch panic"))
320        );
321        let report = worker.report().unwrap();
322        assert_eq!(report.submitted(), 2);
323        assert!(report.coalesced() >= 1);
324        assert_eq!(report.completed(), 1);
325        assert_eq!(report.failed(), 1);
326    }
327
328    #[test]
329    fn drop_cancels_and_joins_in_flight_work() {
330        let gate = Arc::new((Mutex::new(false), Condvar::new()));
331        let operation_gate = Arc::clone(&gate);
332        let worker = BackgroundPrefetchWorker::new(1, "runtime-prefetch-drop", move |_| {
333            let mut released = operation_gate.0.lock().unwrap();
334            while !*released {
335                released = operation_gate.1.wait(released).unwrap();
336            }
337            Ok(())
338        })
339        .unwrap();
340        worker.submit(&id("layer"), false).unwrap();
341        while worker.report().unwrap().started() == 0 {
342            thread::yield_now();
343        }
344        let (finished_tx, finished_rx) = mpsc::channel();
345        thread::spawn(move || {
346            drop(worker);
347            finished_tx.send(()).unwrap();
348        });
349        assert!(finished_rx.recv_timeout(Duration::from_millis(20)).is_err());
350        *gate.0.lock().unwrap() = true;
351        gate.1.notify_all();
352        finished_rx.recv_timeout(Duration::from_secs(1)).unwrap();
353    }
354
355    #[test]
356    fn nonblocking_drop_retains_active_operation_and_cancels_queued_work() {
357        struct OperationOwner(mpsc::Sender<()>);
358        impl Drop for OperationOwner {
359            fn drop(&mut self) {
360                let _ = self.0.send(());
361            }
362        }
363
364        let gate = Arc::new((Mutex::new(false), Condvar::new()));
365        let operation_gate = Arc::clone(&gate);
366        let (started_tx, started_rx) = mpsc::channel();
367        let (released_tx, released_rx) = mpsc::channel();
368        let owner = OperationOwner(released_tx);
369        let worker = BackgroundPrefetchWorker::new(1, "runtime-prefetch-detach", move |_| {
370            let _retained = &owner;
371            let _ = started_tx.send(());
372            let mut released = operation_gate.0.lock().unwrap();
373            while !*released {
374                released = operation_gate.1.wait(released).unwrap();
375            }
376            Ok(())
377        })
378        .unwrap()
379        .with_nonblocking_drop();
380        worker.submit(&id("active"), false).unwrap();
381        started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
382        worker.submit(&id("queued"), false).unwrap();
383        let shared = Arc::clone(&worker.shared);
384        let (dropped_tx, dropped_rx) = mpsc::channel();
385        thread::spawn(move || {
386            drop(worker);
387            let _ = dropped_tx.send(());
388        });
389        let dropped_before_release = dropped_rx.recv_timeout(Duration::from_secs(1));
390        let owner_still_retained = released_rx.try_recv().is_err();
391        *gate.0.lock().unwrap() = true;
392        gate.1.notify_all();
393        dropped_before_release.unwrap();
394        assert!(owner_still_retained);
395        released_rx.recv_timeout(Duration::from_secs(1)).unwrap();
396        let report = shared.0.lock().unwrap().report();
397        assert_eq!(report.started(), 1);
398        assert_eq!(report.cancelled(), 1);
399        assert!(started_rx.try_recv().is_err());
400    }
401
402    #[test]
403    fn cancellation_fences_active_and_queued_generations() {
404        let gate = Arc::new((Mutex::new(false), Condvar::new()));
405        let operation_gate = Arc::clone(&gate);
406        let worker = Arc::new(
407            BackgroundPrefetchWorker::new(1, "runtime-prefetch-cancel", move |_| {
408                let mut released = operation_gate.0.lock().unwrap();
409                while !*released {
410                    released = operation_gate.1.wait(released).unwrap();
411                }
412                Ok(())
413            })
414            .unwrap(),
415        );
416        worker.submit(&id("active"), false).unwrap();
417        while worker.report().unwrap().started() == 0 {
418            thread::yield_now();
419        }
420        worker.submit(&id("queued"), false).unwrap();
421
422        let cancelling = Arc::clone(&worker);
423        let (cancelled_tx, cancelled_rx) = mpsc::channel();
424        thread::spawn(move || cancelled_tx.send(cancelling.cancel()).unwrap());
425        let mut state = worker.shared.0.lock().unwrap();
426        while state.generation() == 0 {
427            state = worker.shared.1.wait(state).unwrap();
428        }
429        drop(state);
430        *gate.0.lock().unwrap() = true;
431        gate.1.notify_all();
432        cancelled_rx
433            .recv_timeout(Duration::from_secs(1))
434            .unwrap()
435            .unwrap();
436        let report = worker.report().unwrap();
437        assert_eq!(report.started(), 1);
438        assert_eq!(report.completed(), 0);
439        assert_eq!(report.cancelled(), 2);
440    }
441
442    #[test]
443    fn disconnected_notification_rolls_back_admission() {
444        let mut worker =
445            BackgroundPrefetchWorker::new(1, "runtime-prefetch-disconnect", |_| Ok(())).unwrap();
446        worker.sender.send(WorkerMessage::Shutdown).unwrap();
447        worker.worker.take().unwrap().join().unwrap();
448        assert!(matches!(
449            worker.submit(&id("layer"), false),
450            Err(BackgroundPrefetchWorkerError::WorkerDisconnected)
451        ));
452        assert_eq!(worker.report().unwrap().submitted(), 0);
453        worker.cancel().unwrap();
454    }
455}