1use 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
22pub struct BackgroundPrefetchWorker {
24 sender: mpsc::Sender<WorkerMessage>,
25 shared: Arc<(Mutex<PrefetchExecutionState<String>>, Condvar)>,
26 worker: Option<JoinHandle<()>>,
27}
28
29impl BackgroundPrefetchWorker {
30 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 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 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 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 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 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#[derive(Debug, thiserror::Error)]
230pub enum BackgroundPrefetchWorkerError {
231 #[error("background prefetch worker state is poisoned")]
233 StatePoisoned,
234 #[error("background prefetch worker disconnected")]
236 WorkerDisconnected,
237 #[error("background prefetch of {id} failed: {message}")]
239 OperationFailed {
240 id: OffloadUnitId,
242 message: String,
244 },
245 #[error(transparent)]
247 State(#[from] eredu_core::residency::PrefetchStateError),
248 #[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}