common/long_operation.rs
1// Generated by Qleany v1.8.0 from long_operation.tera
2//! This module provides a framework for managing long-running operations with the ability to track
3//! status, progress, and enable cancellation. It includes the infrastructure for defining, executing,
4//! and monitoring such operations. For undoable operations, it is recommended to use the Undo/Redo framework.
5//!
6//! # Components:
7//!
8//! - **OperationStatus**: Enum representing the state of an operation.
9//! - **OperationProgress**: Struct holding details about the progress of an operation.
10//! - **LongOperation**: Trait that must be implemented by any long-running operation.
11//! - **LongOperationManager**: Manager that orchestrates the execution, tracking, and cleanup of multiple operations.
12//!
13//! # Usage:
14//!
15//! 1. Implement the `LongOperation` trait for your task.
16//! 2. Use `LongOperationManager` to start, track, and manage your operations.
17//! 3. Access methods like:
18//! - `start_operation` to start new operations.
19//! - `get_operation_status`, `get_operation_progress` to query operation details.
20//! - `cancel_operation` to cancel operations.
21//! - `cleanup_finished_operations` to remove completed or cancelled operations.
22//!
23//! # Example:
24//!
25//! ```rust,ignore
26//! // Define your long-running operation
27//! use std::sync::Arc;
28//! use std::sync::atomic::{AtomicBool, Ordering};
29//! use std::thread;
30//! use std::time::Duration;
31//! use common::long_operation::{LongOperation, LongOperationManager, OperationProgress};
32//!
33//! pub struct MyOperation {
34//! pub total_steps: usize,
35//! }
36//!
37//! impl LongOperation for MyOperation {
38//! fn execute(
39//! &self,
40//! progress_callback: Box<dyn Fn(OperationProgress) + Send>,
41//! cancel_flag: Arc<AtomicBool>,
42//! ) -> Result<(), String> {
43//! for i in 0..self.total_steps {
44//! if cancel_flag.load(Ordering::Relaxed) {
45//! return Err("Operation cancelled".to_string());
46//! }
47//! thread::sleep(Duration::from_millis(500));
48//! progress_callback(OperationProgress::new(
49//! (i as f32 / self.total_steps as f32) * 100.0,
50//! Some(format!("Step {}/{}", i + 1, self.total_steps)),
51//! ));
52//! }
53//! Ok(())
54//! }
55//! }
56//!
57//! let manager = LongOperationManager::new();
58//! let my_operation = MyOperation { total_steps: 5 };
59//! let operation_id = manager.start_operation(my_operation);
60//!
61//! while let Some(status) = manager.get_operation_status(&operation_id) {
62//! println!("{:?}", status);
63//! thread::sleep(Duration::from_millis(100));
64//! }
65//! ```
66//!
67//! # Notes:
68//!
69//! - Thread-safety is ensured through the use of `Arc<Mutex<T>>` and `AtomicBool`.
70//! - Operations run in their own threads, ensuring non-blocking execution.
71//! - Proper cleanup of finished operations is encouraged using `cleanup_finished_operations`.
72//!
73//! # Definitions:
74//!
75//! ## `OperationStatus`
76//! Represents the state of an operation. Possible states are:
77//! - `Running`: Operation is ongoing.
78//! - `Completed`: Operation finished successfully.
79//! - `Cancelled`: Operation was cancelled by the user.
80//! - `Failed(String)`: Operation failed with an error message.
81//!
82//! ## `OperationProgress`
83//! Describes the progress of an operation, including:
84//! - `percentage` (0.0 to 100.0): Indicates completion progress.
85//! - `message`: Optional user-defined progress description.
86//!
87//! ## `LongOperation` Trait
88//! Any custom long-running operation must implement this trait:
89//! - `execute`: Defines the operation logic, accepting a progress callback and cancellation flag.
90//!
91//! ## `LongOperationManager`
92//! Provides APIs to manage operations, including:
93//! - `start_operation`: Starts a new operation and returns its unique ID.
94//! - `get_operation_status`: Queries the current status of an operation.
95//! - `get_operation_progress`: Retrieves the progress of an operation.
96//! - `cancel_operation`: Cancels an operation.
97//! - `cleanup_finished_operations`: Removes completed or cancelled operations to free resources.
98//!
99//! ## Example Operation: FileProcessingOperation
100//! Represents a long-running operation to process files. Demonstrates typical usage of the framework.
101//!
102//! - **Fields**:
103//! - `file_path`: Path of the file to process.
104//! - `total_files`: Number of files to process.
105//! - **Behavior**:
106//! Simulates file processing with periodic progress updates. Supports cancellation.
107//!
108//!
109
110// Generated by Qleany. Edit at your own risk! Be careful when regenerating this file
111// as changes will be lost.
112
113use crate::event::{Event, EventHub, LongOperationEvent, Origin};
114use anyhow::Result;
115use std::collections::{HashMap, HashSet};
116use std::sync::{
117 Arc, Condvar, Mutex,
118 atomic::{AtomicBool, Ordering},
119};
120use std::thread;
121use std::time::Duration;
122
123// Status of a long operation
124#[derive(Debug, Clone, PartialEq)]
125pub enum OperationStatus {
126 Running,
127 Completed,
128 Cancelled,
129 Failed(String),
130}
131
132// Progress information
133#[derive(Debug, Clone)]
134pub struct OperationProgress {
135 pub percentage: f32, // 0.0 to 100.0
136 pub message: Option<String>,
137}
138
139impl OperationProgress {
140 pub fn new(percentage: f32, message: Option<String>) -> Self {
141 Self {
142 percentage: percentage.clamp(0.0, 100.0),
143 message,
144 }
145 }
146}
147
148// Trait that long operations must implement
149pub trait LongOperation: Send + 'static {
150 type Output: Send + Sync + 'static + serde::Serialize;
151
152 fn execute(
153 &self,
154 progress_callback: Box<dyn Fn(OperationProgress) + Send>,
155 cancel_flag: Arc<AtomicBool>,
156 ) -> Result<Self::Output>;
157}
158
159// Trait for operation handles (type-erased)
160trait OperationHandleTrait: Send {
161 fn get_status(&self) -> OperationStatus;
162 fn get_progress(&self) -> OperationProgress;
163 fn cancel(&self);
164 fn is_finished(&self) -> bool;
165}
166
167/// Lock a mutex, recovering from poisoning by returning the inner value.
168///
169/// If a thread panicked while holding the lock, the data may be in an
170/// inconsistent state, but for status/progress tracking this is preferable
171/// to propagating the panic.
172fn lock_or_recover<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
173 mutex
174 .lock()
175 .unwrap_or_else(|poisoned| poisoned.into_inner())
176}
177
178/// The completion signal every operation publishes to when it ends.
179///
180/// This is what lets a caller **block until** an operation finishes instead of
181/// re-checking it on a timer. A worker marks its id finished here as the very
182/// last thing it does — after the result is stored, the event emitted, and the
183/// final status set — so a waiter woken by it is guaranteed to observe all
184/// three. In particular, `is_finished(id) == true` implies
185/// [`LongOperationManager::get_operation_result`] already has the result (for a
186/// `Completed` operation), which is what makes an event-free blocking wait
187/// correct.
188///
189/// Obtain one via [`LongOperationManager::completion_signal`] and block on it
190/// **after** releasing any lock guarding the manager — see that method.
191pub struct OperationCompletion {
192 /// Ids that reached a final status. Deliberately never pruned: it mirrors
193 /// the manager's `results` map, so a wait on an already-finished (even
194 /// cleaned-up) operation returns at once instead of blocking on a signal
195 /// that can no longer come.
196 finished: Mutex<HashSet<String>>,
197 condvar: Condvar,
198}
199
200impl OperationCompletion {
201 fn new() -> Self {
202 Self {
203 finished: Mutex::new(HashSet::new()),
204 condvar: Condvar::new(),
205 }
206 }
207
208 /// Publish `id` as finished and wake every waiter.
209 ///
210 /// `notify_all`, not `notify_one`: waiters block on one shared condvar but
211 /// each waits for its *own* id, so waking a single arbitrary thread could
212 /// wake one waiting on a different operation and leave the right one asleep.
213 fn mark_finished(&self, id: &str) {
214 let mut finished = lock_or_recover(&self.finished);
215 finished.insert(id.to_string());
216 drop(finished);
217 self.condvar.notify_all();
218 }
219
220 /// Has `id` already reached a final status?
221 pub fn is_finished(&self, id: &str) -> bool {
222 lock_or_recover(&self.finished).contains(id)
223 }
224
225 /// Block until `id` reaches a final status; `true` once it has.
226 ///
227 /// There is no lost-wakeup window: the finished-set is inspected while
228 /// holding the very mutex the condvar is paired with, and a worker must take
229 /// that same mutex to publish — so an operation that ends between a caller's
230 /// check and its wait is already recorded and returns immediately.
231 ///
232 /// `timeout` bounds one wait, not the whole call, and exists only as a
233 /// backstop for a signal that can never arrive (e.g. a worker killed before
234 /// publishing). `None` waits indefinitely.
235 pub fn wait_for(&self, id: &str, timeout: Option<Duration>) -> bool {
236 let mut finished = lock_or_recover(&self.finished);
237 loop {
238 if finished.contains(id) {
239 return true;
240 }
241 match timeout {
242 Some(t) => {
243 let (guard, outcome) = self
244 .condvar
245 .wait_timeout(finished, t)
246 .unwrap_or_else(|poisoned| poisoned.into_inner());
247 finished = guard;
248 if outcome.timed_out() {
249 return finished.contains(id);
250 }
251 }
252 None => {
253 finished = self
254 .condvar
255 .wait(finished)
256 .unwrap_or_else(|poisoned| poisoned.into_inner());
257 }
258 }
259 }
260 }
261}
262
263impl std::fmt::Debug for OperationCompletion {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 f.debug_struct("OperationCompletion")
266 .field("finished_len", &lock_or_recover(&self.finished).len())
267 .finish()
268 }
269}
270
271// Concrete handle implementation
272struct OperationHandle {
273 status: Arc<Mutex<OperationStatus>>,
274 progress: Arc<Mutex<OperationProgress>>,
275 cancel_flag: Arc<AtomicBool>,
276 _join_handle: thread::JoinHandle<()>,
277}
278
279impl OperationHandleTrait for OperationHandle {
280 fn get_status(&self) -> OperationStatus {
281 lock_or_recover(&self.status).clone()
282 }
283
284 fn get_progress(&self) -> OperationProgress {
285 lock_or_recover(&self.progress).clone()
286 }
287
288 fn cancel(&self) {
289 self.cancel_flag.store(true, Ordering::Relaxed);
290 let mut status = lock_or_recover(&self.status);
291 if matches!(*status, OperationStatus::Running) {
292 *status = OperationStatus::Cancelled;
293 }
294 }
295
296 fn is_finished(&self) -> bool {
297 matches!(
298 self.get_status(),
299 OperationStatus::Completed | OperationStatus::Cancelled | OperationStatus::Failed(_)
300 )
301 }
302}
303
304// Manager for long operations
305pub struct LongOperationManager {
306 operations: Arc<Mutex<HashMap<String, Box<dyn OperationHandleTrait>>>>,
307 next_id: Arc<Mutex<u64>>,
308 results: Arc<Mutex<HashMap<String, String>>>, // Store serialized results
309 event_hub: Option<Arc<EventHub>>,
310 /// Signalled by each worker as it ends; lets callers block until an
311 /// operation finishes rather than polling. See [`OperationCompletion`].
312 completion: Arc<OperationCompletion>,
313}
314
315impl std::fmt::Debug for LongOperationManager {
316 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317 let operations_len = lock_or_recover(&self.operations).len();
318 let next_id = *lock_or_recover(&self.next_id);
319 let results_len = lock_or_recover(&self.results).len();
320
321 f.debug_struct("LongOperationManager")
322 .field("operations_len", &operations_len)
323 .field("next_id", &next_id)
324 .field("results_len", &results_len)
325 .field("event_hub_set", &self.event_hub.is_some())
326 .finish()
327 }
328}
329
330impl LongOperationManager {
331 pub fn new() -> Self {
332 Self {
333 operations: Arc::new(Mutex::new(HashMap::new())),
334 next_id: Arc::new(Mutex::new(0)),
335 results: Arc::new(Mutex::new(HashMap::new())),
336 event_hub: None,
337 completion: Arc::new(OperationCompletion::new()),
338 }
339 }
340
341 /// Inject the event hub to allow sending long operation related events
342 pub fn set_event_hub(&mut self, event_hub: &Arc<EventHub>) {
343 self.event_hub = Some(Arc::clone(event_hub));
344 }
345
346 /// Start a new long operation and return its ID
347 pub fn start_operation<Op: LongOperation>(&self, operation: Op) -> String {
348 let id = {
349 let mut next_id = lock_or_recover(&self.next_id);
350 *next_id += 1;
351 format!("op_{}", *next_id)
352 };
353
354 // Emit started event
355 if let Some(event_hub) = &self.event_hub {
356 event_hub.send_event(Event {
357 origin: Origin::LongOperation(LongOperationEvent::Started),
358 ids: vec![],
359 data: Some(id.clone()),
360 });
361 }
362
363 let status = Arc::new(Mutex::new(OperationStatus::Running));
364 let progress = Arc::new(Mutex::new(OperationProgress::new(0.0, None)));
365 let cancel_flag = Arc::new(AtomicBool::new(false));
366
367 let status_clone = status.clone();
368 let progress_clone = progress.clone();
369 let cancel_flag_clone = cancel_flag.clone();
370 let results_clone = self.results.clone();
371 let id_clone = id.clone();
372 let event_hub_opt = self.event_hub.clone();
373 let completion_clone = self.completion.clone();
374
375 let join_handle = thread::spawn(move || {
376 let progress_callback = {
377 let progress = progress_clone.clone();
378 let event_hub_opt = event_hub_opt.clone();
379 let id_for_cb = id_clone.clone();
380 Box::new(move |prog: OperationProgress| {
381 *lock_or_recover(&progress) = prog.clone();
382 if let Some(event_hub) = &event_hub_opt {
383 let payload = serde_json::json!({
384 "id": id_for_cb,
385 "percentage": prog.percentage,
386 "message": prog.message,
387 })
388 .to_string();
389 event_hub.send_event(Event {
390 origin: Origin::LongOperation(LongOperationEvent::Progress),
391 ids: vec![],
392 data: Some(payload),
393 });
394 }
395 }) as Box<dyn Fn(OperationProgress) + Send>
396 };
397
398 // Catch a panic in the operation body so a background failure is
399 // reported as `Failed` (with the panic message) instead of leaving
400 // the operation stuck at `Running` and the thread silently dead.
401 let operation_result =
402 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
403 operation.execute(progress_callback, cancel_flag_clone.clone())
404 })) {
405 Ok(result) => result,
406 Err(payload) => {
407 let msg = payload
408 .downcast_ref::<&str>()
409 .map(|s| s.to_string())
410 .or_else(|| payload.downcast_ref::<String>().cloned())
411 .unwrap_or_else(|| "operation panicked".to_string());
412 Err(anyhow::anyhow!("operation panicked: {msg}"))
413 }
414 };
415
416 let final_status = if cancel_flag_clone.load(Ordering::Relaxed) {
417 OperationStatus::Cancelled
418 } else {
419 match &operation_result {
420 Ok(result) => {
421 // Store the result
422 if let Ok(serialized) = serde_json::to_string(result) {
423 let mut results = lock_or_recover(&results_clone);
424 results.insert(id_clone.clone(), serialized);
425 }
426 OperationStatus::Completed
427 }
428 Err(e) => OperationStatus::Failed(e.to_string()),
429 }
430 };
431
432 // Emit final status event
433 if let Some(event_hub) = &event_hub_opt {
434 let (event, data) = match &final_status {
435 OperationStatus::Completed => (
436 LongOperationEvent::Completed,
437 serde_json::json!({"id": id_clone}).to_string(),
438 ),
439 OperationStatus::Cancelled => (
440 LongOperationEvent::Cancelled,
441 serde_json::json!({"id": id_clone}).to_string(),
442 ),
443 OperationStatus::Failed(err) => (
444 LongOperationEvent::Failed,
445 serde_json::json!({"id": id_clone, "error": err}).to_string(),
446 ),
447 OperationStatus::Running => (
448 LongOperationEvent::Progress,
449 serde_json::json!({"id": id_clone}).to_string(),
450 ),
451 };
452 event_hub.send_event(Event {
453 origin: Origin::LongOperation(event),
454 ids: vec![],
455 data: Some(data),
456 });
457 }
458
459 *lock_or_recover(&status_clone) = final_status;
460
461 // Publish completion **last** — after the result is stored, the
462 // event emitted and the final status written — so any thread woken
463 // by this observes a fully-settled operation rather than racing the
464 // bookkeeping that follows it.
465 completion_clone.mark_finished(&id_clone);
466 });
467
468 let handle = OperationHandle {
469 status,
470 progress,
471 cancel_flag,
472 _join_handle: join_handle,
473 };
474
475 lock_or_recover(&self.operations).insert(id.clone(), Box::new(handle));
476
477 id
478 }
479
480 /// Get the status of an operation
481 pub fn get_operation_status(&self, id: &str) -> Option<OperationStatus> {
482 let operations = lock_or_recover(&self.operations);
483 operations.get(id).map(|handle| handle.get_status())
484 }
485
486 /// Get the progress of an operation
487 pub fn get_operation_progress(&self, id: &str) -> Option<OperationProgress> {
488 let operations = lock_or_recover(&self.operations);
489 operations.get(id).map(|handle| handle.get_progress())
490 }
491
492 /// Cancel an operation
493 pub fn cancel_operation(&self, id: &str) -> bool {
494 let operations = lock_or_recover(&self.operations);
495 if let Some(handle) = operations.get(id) {
496 handle.cancel();
497 // Emit cancelled event immediately
498 if let Some(event_hub) = &self.event_hub {
499 let payload = serde_json::json!({"id": id}).to_string();
500 event_hub.send_event(Event {
501 origin: Origin::LongOperation(LongOperationEvent::Cancelled),
502 ids: vec![],
503 data: Some(payload),
504 });
505 }
506 true
507 } else {
508 false
509 }
510 }
511
512 /// Check if an operation is finished
513 pub fn is_operation_finished(&self, id: &str) -> Option<bool> {
514 let operations = lock_or_recover(&self.operations);
515 operations.get(id).map(|handle| handle.is_finished())
516 }
517
518 /// Remove finished operations from memory
519 pub fn cleanup_finished_operations(&self) {
520 let mut operations = lock_or_recover(&self.operations);
521 operations.retain(|_, handle| !handle.is_finished());
522 }
523
524 /// Get list of all operation IDs
525 pub fn list_operations(&self) -> Vec<String> {
526 let operations = lock_or_recover(&self.operations);
527 operations.keys().cloned().collect()
528 }
529
530 /// Get summary of all operations
531 pub fn get_operations_summary(&self) -> Vec<(String, OperationStatus, OperationProgress)> {
532 let operations = lock_or_recover(&self.operations);
533 operations
534 .iter()
535 .map(|(id, handle)| (id.clone(), handle.get_status(), handle.get_progress()))
536 .collect()
537 }
538
539 /// Store an operation result
540 pub fn store_operation_result<T: serde::Serialize>(&self, id: &str, result: T) -> Result<()> {
541 let serialized = serde_json::to_string(&result)?;
542 let mut results = lock_or_recover(&self.results);
543 results.insert(id.to_string(), serialized);
544 Ok(())
545 }
546
547 /// Get an operation result
548 pub fn get_operation_result(&self, id: &str) -> Option<String> {
549 let results = lock_or_recover(&self.results);
550 results.get(id).cloned()
551 }
552
553 /// A cloneable handle to the completion signal — the supported way to
554 /// **block** until an operation finishes instead of re-checking it on a
555 /// timer.
556 ///
557 /// Handed out as an `Arc` by design. A manager is normally reached through a
558 /// shared lock, and blocking while holding that lock would stall every other
559 /// operation query for as long as the wait lasts. So take the handle, drop
560 /// the manager lock, *then* wait:
561 ///
562 /// ```rust,ignore
563 /// let completion = ctx.long_operation_manager.lock().completion_signal();
564 /// // manager lock released here
565 /// completion.wait_for(&op_id, Some(Duration::from_secs(30)));
566 /// let result = ctx.long_operation_manager.lock().get_operation_result(&op_id);
567 /// ```
568 pub fn completion_signal(&self) -> Arc<OperationCompletion> {
569 Arc::clone(&self.completion)
570 }
571
572 /// Block until `id` finishes; `true` once it has.
573 ///
574 /// Convenience for an owner holding the manager directly. **Do not call this
575 /// through a shared lock** — it blocks for the operation's whole duration and
576 /// would hold that lock the entire time; take
577 /// [`completion_signal`](Self::completion_signal) and wait on that instead.
578 pub fn wait_for_operation(&self, id: &str, timeout: Option<Duration>) -> bool {
579 self.completion.wait_for(id, timeout)
580 }
581}
582
583impl Default for LongOperationManager {
584 fn default() -> Self {
585 Self::new()
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592 use anyhow::anyhow;
593 use std::time::Duration;
594
595 // Example implementation of a long operation
596 pub struct FileProcessingOperation {
597 pub _file_path: String,
598 pub total_files: usize,
599 }
600
601 impl LongOperation for FileProcessingOperation {
602 type Output = ();
603
604 fn execute(
605 &self,
606 progress_callback: Box<dyn Fn(OperationProgress) + Send>,
607 cancel_flag: Arc<AtomicBool>,
608 ) -> Result<Self::Output> {
609 for i in 0..self.total_files {
610 // Check if operation was cancelled
611 if cancel_flag.load(Ordering::Relaxed) {
612 return Err(anyhow!("Operation was cancelled".to_string()));
613 }
614
615 // Simulate work
616 thread::sleep(Duration::from_millis(500));
617
618 // Report progress
619 let percentage = (i as f32 / self.total_files as f32) * 100.0;
620 progress_callback(OperationProgress::new(
621 percentage,
622 Some(format!("Processing file {} of {}", i + 1, self.total_files)),
623 ));
624 }
625
626 // Final progress
627 progress_callback(OperationProgress::new(100.0, Some("Completed".to_string())));
628 Ok(())
629 }
630 }
631
632 /// An operation that finishes at once and returns a value — the shape of a
633 /// small import, and the case a blocking wait must add no latency to.
634 pub struct InstantOperation;
635
636 impl LongOperation for InstantOperation {
637 type Output = u32;
638
639 fn execute(
640 &self,
641 _progress_callback: Box<dyn Fn(OperationProgress) + Send>,
642 _cancel_flag: Arc<AtomicBool>,
643 ) -> Result<Self::Output> {
644 Ok(42)
645 }
646 }
647
648 /// The whole point of the completion signal: a caller is woken the moment the
649 /// worker publishes, with no polling interval and no fixed sleep floor. A
650 /// trivial operation must therefore settle in far less than a timer tick —
651 /// this is what stops N sequential small operations costing N × a poll period.
652 #[test]
653 fn wait_for_returns_as_soon_as_the_operation_finishes() {
654 let manager = LongOperationManager::new();
655 let completion = manager.completion_signal();
656 let started = std::time::Instant::now();
657 let op_id = manager.start_operation(InstantOperation);
658
659 assert!(completion.wait_for(&op_id, Some(Duration::from_secs(5))));
660 let elapsed = started.elapsed();
661
662 // The result is stored *before* completion is published, so a woken
663 // waiter can always read it straight back.
664 assert_eq!(
665 manager.get_operation_result(&op_id).as_deref(),
666 Some("42"),
667 "completion must imply the result is already retrievable"
668 );
669 assert!(
670 elapsed < Duration::from_millis(50),
671 "a trivial operation must not wait out a polling tick (took {elapsed:?})"
672 );
673 }
674
675 /// Waiting on an operation that has already finished returns at once, instead
676 /// of blocking for a signal that was sent before the caller arrived.
677 #[test]
678 fn wait_for_an_already_finished_operation_returns_immediately() {
679 let manager = LongOperationManager::new();
680 let completion = manager.completion_signal();
681 let op_id = manager.start_operation(InstantOperation);
682 assert!(completion.wait_for(&op_id, Some(Duration::from_secs(5))));
683
684 // The finished record persists, so a second wait is instant.
685 let started = std::time::Instant::now();
686 assert!(completion.wait_for(&op_id, Some(Duration::from_secs(5))));
687 assert!(started.elapsed() < Duration::from_millis(50));
688 assert!(completion.is_finished(&op_id));
689 }
690
691 /// The timeout bounds one wait; it is not a deadline for the operation. A
692 /// still-running operation reports `false` and leaves the caller free to wait
693 /// again.
694 #[test]
695 fn wait_for_times_out_while_the_operation_is_still_running() {
696 let manager = LongOperationManager::new();
697 let completion = manager.completion_signal();
698 let op_id = manager.start_operation(FileProcessingOperation {
699 _file_path: "/tmp/test".to_string(),
700 total_files: 5,
701 });
702
703 assert!(
704 !completion.wait_for(&op_id, Some(Duration::from_millis(50))),
705 "a running operation has published no completion"
706 );
707 assert!(!completion.is_finished(&op_id));
708 manager.cancel_operation(&op_id);
709 }
710
711 /// A cancelled operation must still publish completion — otherwise a caller
712 /// blocked on it would be stranded forever.
713 #[test]
714 fn a_cancelled_operation_still_publishes_completion() {
715 let manager = LongOperationManager::new();
716 let completion = manager.completion_signal();
717 let op_id = manager.start_operation(FileProcessingOperation {
718 _file_path: "/tmp/test".to_string(),
719 total_files: 5,
720 });
721 manager.cancel_operation(&op_id);
722
723 assert!(
724 completion.wait_for(&op_id, Some(Duration::from_secs(5))),
725 "cancellation must wake a blocked waiter, not strand it"
726 );
727 }
728
729 #[test]
730 fn test_operation_manager() {
731 let manager = LongOperationManager::new();
732
733 let operation = FileProcessingOperation {
734 _file_path: "/tmp/test".to_string(),
735 total_files: 5,
736 };
737
738 let op_id = manager.start_operation(operation);
739
740 // Check initial status
741 assert_eq!(
742 manager.get_operation_status(&op_id),
743 Some(OperationStatus::Running)
744 );
745
746 // Wait a bit and check progress
747 thread::sleep(Duration::from_millis(100));
748 let progress = manager.get_operation_progress(&op_id);
749 assert!(progress.is_some());
750
751 // Test cancellation
752 assert!(manager.cancel_operation(&op_id));
753 thread::sleep(Duration::from_millis(100));
754 assert_eq!(
755 manager.get_operation_status(&op_id),
756 Some(OperationStatus::Cancelled)
757 );
758 }
759}