1use 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
25pub 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 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 pub fn with_nonblocking_drop(mut self) -> Self {
71 self.nonblocking_drop = true;
72 self
73 }
74
75 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 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 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 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 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#[derive(Debug, thiserror::Error)]
266pub enum BackgroundPrefetchWorkerError {
267 #[error("background prefetch worker state is poisoned")]
269 StatePoisoned,
270 #[error("background prefetch worker disconnected")]
272 WorkerDisconnected,
273 #[error("background prefetch of {id} failed: {message}")]
275 OperationFailed {
276 id: OffloadUnitId,
278 message: String,
280 },
281 #[error(transparent)]
283 State(#[from] eredu_core::residency::PrefetchStateError),
284 #[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}