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