1use std::path::{Path, PathBuf};
45use std::sync::atomic::Ordering;
46use std::sync::Arc;
47use std::thread::{self, JoinHandle};
48
49use crate::shared_atomic::{SharedAtomicBool, SharedAtomicError, SharedAtomicU64};
50use crate::shared_cell::{SharedCell, SharedCellError};
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum ProgressTaskError {
54 Atomic(SharedAtomicError),
55 Cell(SharedCellError),
56}
57
58impl From<SharedAtomicError> for ProgressTaskError {
59 fn from(e: SharedAtomicError) -> Self { Self::Atomic(e) }
60}
61impl From<SharedCellError> for ProgressTaskError {
62 fn from(e: SharedCellError) -> Self { Self::Cell(e) }
63}
64
65fn progress_path(base: &Path) -> PathBuf {
66 let mut p = base.to_path_buf();
67 let stem = p.file_name().unwrap().to_string_lossy().to_string();
68 p.set_file_name(format!("{stem}.progress.bin"));
69 p
70}
71fn total_path(base: &Path) -> PathBuf {
72 let mut p = base.to_path_buf();
73 let stem = p.file_name().unwrap().to_string_lossy().to_string();
74 p.set_file_name(format!("{stem}.total.bin"));
75 p
76}
77fn done_path(base: &Path) -> PathBuf {
78 let mut p = base.to_path_buf();
79 let stem = p.file_name().unwrap().to_string_lossy().to_string();
80 p.set_file_name(format!("{stem}.done.bin"));
81 p
82}
83fn result_path(base: &Path) -> PathBuf {
84 let mut p = base.to_path_buf();
85 let stem = p.file_name().unwrap().to_string_lossy().to_string();
86 p.set_file_name(format!("{stem}.result.bin"));
87 p
88}
89
90pub struct ProgressReporter {
94 progress: Arc<SharedAtomicU64>,
95}
96
97impl ProgressReporter {
98 #[inline]
102 pub fn advance(&self, n: u64) -> u64 {
103 self.progress.fetch_add(n, Ordering::Relaxed)
104 }
105
106 #[inline]
110 pub fn set(&self, v: u64) {
111 self.progress.store(v, Ordering::Relaxed);
112 }
113
114 #[inline]
116 pub fn current(&self) -> u64 {
117 self.progress.load(Ordering::Relaxed)
118 }
119}
120
121pub struct ProgressTask<R: Copy + 'static> {
122 progress: Arc<SharedAtomicU64>,
123 total: Arc<SharedAtomicU64>,
124 done: Arc<SharedAtomicBool>,
125 result: Arc<SharedCell<R>>,
126 header_sidecar: subetha_core::HandshakeHeader,
127 ring_sidecar: Box<subetha_core::ObservationRing>,
128}
129
130impl<R: Copy + Send + Sync + 'static> subetha_sidecar::AdaptiveInstance for ProgressTask<R> {
131 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
132 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
133 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
134 Box::new(subetha_sidecar::NoMigrationPolicy)
135 }
136}
137
138impl<R: Copy + 'static> ProgressTask<R> {
139 pub fn create(
143 base_path: impl AsRef<Path>,
144 initial_result: R,
145 ) -> Result<Self, ProgressTaskError> {
146 let base = base_path.as_ref();
147 let progress = Arc::new(SharedAtomicU64::create(progress_path(base), 0)?);
148 let total = Arc::new(SharedAtomicU64::create(total_path(base), 0)?);
149 let done = Arc::new(SharedAtomicBool::create(done_path(base), false)?);
150 let result = Arc::new(SharedCell::<R>::create(result_path(base))?);
151 result.set(initial_result);
152 Ok(Self {
153 progress, total, done, result,
154 header_sidecar: subetha_core::HandshakeHeader::new(),
155 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
156 })
157 }
158
159 pub fn open(base_path: impl AsRef<Path>) -> Result<Self, ProgressTaskError> {
162 let base = base_path.as_ref();
163 let progress = Arc::new(SharedAtomicU64::open(progress_path(base))?);
164 let total = Arc::new(SharedAtomicU64::open(total_path(base))?);
165 let done = Arc::new(SharedAtomicBool::open(done_path(base))?);
166 let result = Arc::new(SharedCell::<R>::open(result_path(base))?);
167 Ok(Self {
168 progress, total, done, result,
169 header_sidecar: subetha_core::HandshakeHeader::new(),
170 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
171 })
172 }
173
174 pub fn begin(&self, total: u64) -> ProgressReporter {
178 self.progress.store(0, Ordering::Relaxed);
179 self.done.store(false, Ordering::Release);
180 self.total.store(total, Ordering::Release);
181 self.ring_sidecar
182 .push_op(crate::sidecar_ops::progress::OP_ADVANCE, 0);
183 ProgressReporter { progress: self.progress.clone() }
184 }
185
186 pub fn complete(&self, result: R) {
190 self.result.set(result);
191 self.done.store(true, Ordering::Release);
194 self.ring_sidecar
195 .push_op(crate::sidecar_ops::progress::OP_COMPLETE, 0);
196 }
197
198 pub fn fraction_complete(&self) -> f64 {
201 let p = self.progress.load(Ordering::Relaxed);
202 let t = self.total.load(Ordering::Acquire);
203 if t == 0 { return 0.0; }
204 (p as f64 / t as f64).min(1.0)
205 }
206
207 #[inline]
209 pub fn current_progress(&self) -> u64 {
210 self.progress.load(Ordering::Relaxed)
211 }
212
213 #[inline]
215 pub fn total(&self) -> u64 {
216 self.total.load(Ordering::Acquire)
217 }
218
219 #[inline]
221 pub fn is_done(&self) -> bool {
222 self.done.load(Ordering::Acquire)
223 }
224
225 pub fn read_result(&self) -> Option<R> {
229 let r = if self.is_done() {
230 Some(self.result.get())
231 } else {
232 None
233 };
234 self.ring_sidecar.push_op(
235 crate::sidecar_ops::progress::OP_READ,
236 if r.is_none() { 2 } else { 0 },
237 );
238 r
239 }
240
241 #[inline]
244 pub fn peek_result(&self) -> R {
245 self.result.get()
246 }
247
248 pub fn run<F>(&self, total: u64, work: F) -> R
252 where F: FnOnce(&ProgressReporter) -> R
253 {
254 let reporter = self.begin(total);
255 let r = work(&reporter);
256 self.complete(r);
257 r
258 }
259
260 pub fn spawn<F>(self: &Arc<Self>, total: u64, work: F) -> JoinHandle<()>
264 where
265 F: FnOnce(&ProgressReporter) -> R + Send + 'static,
266 R: Send + Sync,
267 {
268 let me = self.clone();
269 thread::spawn(move || {
270 me.run(total, work);
271 })
272 }
273
274 pub fn flush(&self) -> Result<(), ProgressTaskError> {
276 self.progress.flush()?;
277 self.total.flush()?;
278 self.done.flush()?;
279 self.result.flush()?;
280 Ok(())
281 }
282
283 pub fn flush_async(&self) -> Result<(), ProgressTaskError> {
288 self.progress.flush_async()?;
289 self.total.flush_async()?;
290 self.done.flush_async()?;
291 self.result.flush_async()?;
292 Ok(())
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use std::time::Duration;
300
301 fn tmp_base(name: &str) -> PathBuf {
302 let mut p = std::env::temp_dir();
303 let pid = std::process::id();
304 p.push(format!("subetha-progress-{name}-{pid}"));
305 p
306 }
307
308 fn cleanup(base: &Path) {
309 std::fs::remove_file(progress_path(base)).ok();
310 std::fs::remove_file(total_path(base)).ok();
311 std::fs::remove_file(done_path(base)).ok();
312 std::fs::remove_file(result_path(base)).ok();
313 }
314
315 #[test]
316 fn create_initial_state_is_zero() {
317 let base = tmp_base("init");
318 let t: ProgressTask<u64> = ProgressTask::create(&base, 0).unwrap();
319 assert_eq!(t.current_progress(), 0);
320 assert_eq!(t.total(), 0);
321 assert!(!t.is_done());
322 assert_eq!(t.fraction_complete(), 0.0);
323 assert_eq!(t.read_result(), None);
324 cleanup(&base);
325 }
326
327 #[test]
328 fn run_advances_progress_and_publishes_result() {
329 let base = tmp_base("run");
330 let t: ProgressTask<u64> = ProgressTask::create(&base, 0).unwrap();
331 let r = t.run(100, |reporter| {
332 for _ in 0..100 {
333 reporter.advance(1);
334 }
335 42
336 });
337 assert_eq!(r, 42);
338 assert!(t.is_done());
339 assert_eq!(t.read_result(), Some(42));
340 assert_eq!(t.fraction_complete(), 1.0);
341 cleanup(&base);
342 }
343
344 #[test]
345 fn observer_sees_monotonic_progress_advance() {
346 use std::sync::atomic::{AtomicBool, Ordering};
347 let base = tmp_base("observe");
348 let t: Arc<ProgressTask<u64>>
349 = Arc::new(ProgressTask::create(&base, 0).unwrap());
350 let go = Arc::new(AtomicBool::new(false));
351
352 let t_w = t.clone();
353 let go_w = go.clone();
354 let worker = thread::spawn(move || {
355 t_w.run(50, |reporter| {
356 go_w.store(true, Ordering::Release);
357 for _ in 0..50 {
358 reporter.advance(1);
359 thread::sleep(Duration::from_micros(100));
360 }
361 7777
362 });
363 });
364 while !go.load(Ordering::Acquire) { thread::yield_now(); }
366
367 let mut observed = Vec::new();
368 for _ in 0..20 {
369 observed.push(t.current_progress());
370 thread::sleep(Duration::from_micros(150));
371 }
372 worker.join().unwrap();
373 for w in observed.windows(2) {
375 assert!(w[0] <= w[1], "progress went backwards: {observed:?}");
376 }
377 assert!(t.is_done());
378 assert_eq!(t.read_result(), Some(7777));
379 cleanup(&base);
380 }
381
382 #[test]
383 fn cross_handle_observer_sees_worker_state() {
384 let base = tmp_base("cross-handle");
385 let worker_h: ProgressTask<u64> = ProgressTask::create(&base, 0).unwrap();
386 let observer_h: ProgressTask<u64> = ProgressTask::open(&base).unwrap();
387 worker_h.run(10, |r| { r.advance(10); 999 });
388 assert!(observer_h.is_done());
389 assert_eq!(observer_h.read_result(), Some(999));
390 assert_eq!(observer_h.fraction_complete(), 1.0);
391 cleanup(&base);
392 }
393
394 #[test]
395 fn fraction_clamps_when_progress_overshoots_total() {
396 let base = tmp_base("overshoot");
397 let t: ProgressTask<u64> = ProgressTask::create(&base, 0).unwrap();
398 let r = t.begin(10);
399 r.advance(15);
400 assert_eq!(t.fraction_complete(), 1.0);
401 cleanup(&base);
402 }
403
404 #[test]
405 fn read_result_returns_none_before_done() {
406 let base = tmp_base("not-done");
407 let t: ProgressTask<u64> = ProgressTask::create(&base, 0).unwrap();
408 let _r = t.begin(100);
409 assert!(!t.is_done());
410 assert_eq!(t.read_result(), None);
411 assert_eq!(t.peek_result(), 0);
413 cleanup(&base);
414 }
415
416 #[test]
417 fn second_run_resets_progress_and_done() {
418 let base = tmp_base("two-runs");
419 let t: ProgressTask<u64> = ProgressTask::create(&base, 0).unwrap();
420 t.run(5, |r| { r.advance(5); 100 });
421 assert!(t.is_done());
422 assert_eq!(t.current_progress(), 5);
423
424 let _r = t.begin(10);
426 assert!(!t.is_done());
427 assert_eq!(t.current_progress(), 0);
428 assert_eq!(t.total(), 10);
429 cleanup(&base);
430 }
431
432 #[test]
433 fn spawn_background_completes_eventually() {
434 let base = tmp_base("spawn");
435 let t: Arc<ProgressTask<u64>>
436 = Arc::new(ProgressTask::create(&base, 0).unwrap());
437 let h = t.spawn(20, |r| {
438 for _ in 0..20 {
439 r.advance(1);
440 thread::sleep(Duration::from_micros(100));
441 }
442 314
443 });
444 h.join().unwrap();
445 assert!(t.is_done());
446 assert_eq!(t.read_result(), Some(314));
447 cleanup(&base);
448 }
449
450 #[test]
451 fn concurrent_observers_all_see_consistent_completion() {
452 let base = tmp_base("multi-observer");
453 let t: Arc<ProgressTask<u64>>
454 = Arc::new(ProgressTask::create(&base, 0).unwrap());
455 let n_observers = 4;
456
457 let t_w = t.clone();
458 let worker = thread::spawn(move || {
459 t_w.run(100, |r| {
460 for _ in 0..100 {
461 r.advance(1);
462 thread::sleep(Duration::from_micros(50));
463 }
464 5555
465 });
466 });
467
468 let mut handles = vec![];
469 for _ in 0..n_observers {
470 let t = t.clone();
471 handles.push(thread::spawn(move || {
472 let mut last = 0u64;
473 while !t.is_done() {
474 let cur = t.current_progress();
475 assert!(cur >= last, "observer saw progress regress");
476 last = cur;
477 thread::sleep(Duration::from_micros(75));
478 }
479 t.read_result()
480 }));
481 }
482 worker.join().unwrap();
483 for h in handles {
484 assert_eq!(h.join().unwrap(), Some(5555));
485 }
486 cleanup(&base);
487 }
488
489 #[test]
490 fn struct_result_round_trip() {
491 #[derive(Clone, Copy, Debug, PartialEq)]
492 #[repr(C)]
493 struct Stats { processed: u64, errors: u32, skipped: u32 }
494 let base = tmp_base("struct");
495 let t: ProgressTask<Stats> = ProgressTask::create(
496 &base, Stats { processed: 0, errors: 0, skipped: 0 },
497 ).unwrap();
498 let r = t.run(50, |reporter| {
499 for _ in 0..50 { reporter.advance(1); }
500 Stats { processed: 50, errors: 2, skipped: 1 }
501 });
502 assert_eq!(r, Stats { processed: 50, errors: 2, skipped: 1 });
503 assert_eq!(t.read_result(), Some(r));
504 cleanup(&base);
505 }
506
507 #[test]
508 fn disk_persistence_completed_task_survives_reopen() {
509 let base = tmp_base("disk");
510 {
511 let t: ProgressTask<u64> = ProgressTask::create(&base, 0).unwrap();
512 t.run(8, |r| { r.advance(8); 8888 });
513 t.flush().unwrap();
514 }
515 let t2: ProgressTask<u64> = ProgressTask::open(&base).unwrap();
516 assert!(t2.is_done());
517 assert_eq!(t2.read_result(), Some(8888));
518 assert_eq!(t2.current_progress(), 8);
519 assert_eq!(t2.total(), 8);
520 cleanup(&base);
521 }
522
523 #[test]
524 fn reporter_set_replaces_progress() {
525 let base = tmp_base("set");
526 let t: ProgressTask<u64> = ProgressTask::create(&base, 0).unwrap();
527 let r = t.begin(1000);
528 r.advance(100);
529 assert_eq!(t.current_progress(), 100);
530 r.set(500); assert_eq!(t.current_progress(), 500);
532 assert_eq!(t.fraction_complete(), 0.5);
533 cleanup(&base);
534 }
535}