common/long_operation.rs
1// Generated by Qleany v1.11.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 — and
168/// **clearing** the poison, so recovery is permanent rather than per-call.
169///
170/// If a thread panicked while holding the lock the data may be in an
171/// inconsistent state, but for the transaction handles, event buffers and
172/// status/progress trackers this guards, that is strictly preferable to
173/// propagating the panic: the release profile unwinds, so a panic inside a long
174/// operation is caught and reported as `Failed` — and every *later* use of the
175/// same mutex would otherwise panic again, turning one reported failure into a
176/// dead process.
177///
178/// `clear_poison` matches the `RwLock` counterparts in
179/// `database::hashmap_store` (`read_or_recover` / `write_or_recover`).
180/// `PoisonError::into_inner` alone leaves the flag set, so a plain
181/// `.lock().unwrap()` elsewhere in the process would still trip on a lock this
182/// function had already recovered.
183pub fn lock_or_recover<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
184 match mutex.lock() {
185 Ok(guard) => guard,
186 Err(poisoned) => {
187 let guard = poisoned.into_inner();
188 mutex.clear_poison();
189 guard
190 }
191 }
192}
193
194/// The completion signal every operation publishes to when it ends.
195///
196/// This is what lets a caller **block until** an operation finishes instead of
197/// re-checking it on a timer. A worker marks its id finished here as the very
198/// last thing it does — after the result is stored, the event emitted, and the
199/// final status set — so a waiter woken by it is guaranteed to observe all
200/// three. In particular, `is_finished(id) == true` implies
201/// [`LongOperationManager::get_operation_result`] already has the result (for a
202/// `Completed` operation), which is what makes an event-free blocking wait
203/// correct.
204///
205/// Obtain one via [`LongOperationManager::completion_signal`] and block on it
206/// **after** releasing any lock guarding the manager — see that method.
207pub struct OperationCompletion {
208 /// Ids that reached a final status. Deliberately never pruned: it mirrors
209 /// the manager's `results` map, so a wait on an already-finished (even
210 /// cleaned-up) operation returns at once instead of blocking on a signal
211 /// that can no longer come.
212 finished: Mutex<HashSet<String>>,
213 condvar: Condvar,
214}
215
216impl OperationCompletion {
217 fn new() -> Self {
218 Self {
219 finished: Mutex::new(HashSet::new()),
220 condvar: Condvar::new(),
221 }
222 }
223
224 /// Publish `id` as finished and wake every waiter.
225 ///
226 /// `notify_all`, not `notify_one`: waiters block on one shared condvar but
227 /// each waits for its *own* id, so waking a single arbitrary thread could
228 /// wake one waiting on a different operation and leave the right one asleep.
229 fn mark_finished(&self, id: &str) {
230 let mut finished = lock_or_recover(&self.finished);
231 finished.insert(id.to_string());
232 drop(finished);
233 self.condvar.notify_all();
234 }
235
236 /// Has `id` already reached a final status?
237 pub fn is_finished(&self, id: &str) -> bool {
238 lock_or_recover(&self.finished).contains(id)
239 }
240
241 /// Block until `id` reaches a final status; `true` once it has.
242 ///
243 /// There is no lost-wakeup window: the finished-set is inspected while
244 /// holding the very mutex the condvar is paired with, and a worker must take
245 /// that same mutex to publish — so an operation that ends between a caller's
246 /// check and its wait is already recorded and returns immediately.
247 ///
248 /// `timeout` bounds one wait, not the whole call, and exists only as a
249 /// backstop for a signal that can never arrive (e.g. a worker killed before
250 /// publishing). `None` waits indefinitely.
251 pub fn wait_for(&self, id: &str, timeout: Option<Duration>) -> bool {
252 let mut finished = lock_or_recover(&self.finished);
253 loop {
254 if finished.contains(id) {
255 return true;
256 }
257 match timeout {
258 Some(t) => {
259 let (guard, outcome) = self
260 .condvar
261 .wait_timeout(finished, t)
262 .unwrap_or_else(|poisoned| poisoned.into_inner());
263 finished = guard;
264 if outcome.timed_out() {
265 return finished.contains(id);
266 }
267 }
268 None => {
269 finished = self
270 .condvar
271 .wait(finished)
272 .unwrap_or_else(|poisoned| poisoned.into_inner());
273 }
274 }
275 }
276 }
277}
278
279impl std::fmt::Debug for OperationCompletion {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 f.debug_struct("OperationCompletion")
282 .field("finished_len", &lock_or_recover(&self.finished).len())
283 .finish()
284 }
285}
286
287// Concrete handle implementation
288struct OperationHandle {
289 status: Arc<Mutex<OperationStatus>>,
290 progress: Arc<Mutex<OperationProgress>>,
291 cancel_flag: Arc<AtomicBool>,
292 _join_handle: thread::JoinHandle<()>,
293}
294
295impl OperationHandleTrait for OperationHandle {
296 fn get_status(&self) -> OperationStatus {
297 lock_or_recover(&self.status).clone()
298 }
299
300 fn get_progress(&self) -> OperationProgress {
301 lock_or_recover(&self.progress).clone()
302 }
303
304 fn cancel(&self) {
305 self.cancel_flag.store(true, Ordering::Relaxed);
306 let mut status = lock_or_recover(&self.status);
307 if matches!(*status, OperationStatus::Running) {
308 *status = OperationStatus::Cancelled;
309 }
310 }
311
312 fn is_finished(&self) -> bool {
313 matches!(
314 self.get_status(),
315 OperationStatus::Completed | OperationStatus::Cancelled | OperationStatus::Failed(_)
316 )
317 }
318}
319
320// Manager for long operations
321pub struct LongOperationManager {
322 operations: Arc<Mutex<HashMap<String, Box<dyn OperationHandleTrait>>>>,
323 next_id: Arc<Mutex<u64>>,
324 results: Arc<Mutex<HashMap<String, String>>>, // Store serialized results
325 event_hub: Option<Arc<EventHub>>,
326 /// Signalled by each worker as it ends; lets callers block until an
327 /// operation finishes rather than polling. See [`OperationCompletion`].
328 completion: Arc<OperationCompletion>,
329}
330
331impl std::fmt::Debug for LongOperationManager {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 let operations_len = lock_or_recover(&self.operations).len();
334 let next_id = *lock_or_recover(&self.next_id);
335 let results_len = lock_or_recover(&self.results).len();
336
337 f.debug_struct("LongOperationManager")
338 .field("operations_len", &operations_len)
339 .field("next_id", &next_id)
340 .field("results_len", &results_len)
341 .field("event_hub_set", &self.event_hub.is_some())
342 .finish()
343 }
344}
345
346impl LongOperationManager {
347 pub fn new() -> Self {
348 Self {
349 operations: Arc::new(Mutex::new(HashMap::new())),
350 next_id: Arc::new(Mutex::new(0)),
351 results: Arc::new(Mutex::new(HashMap::new())),
352 event_hub: None,
353 completion: Arc::new(OperationCompletion::new()),
354 }
355 }
356
357 /// Inject the event hub to allow sending long operation related events
358 pub fn set_event_hub(&mut self, event_hub: &Arc<EventHub>) {
359 self.event_hub = Some(Arc::clone(event_hub));
360 }
361
362 /// Start a new long operation and return its ID
363 pub fn start_operation<Op: LongOperation>(&self, operation: Op) -> String {
364 let id = {
365 let mut next_id = lock_or_recover(&self.next_id);
366 *next_id += 1;
367 format!("op_{}", *next_id)
368 };
369
370 // Emit started event
371 if let Some(event_hub) = &self.event_hub {
372 event_hub.send_event(Event {
373 origin: Origin::LongOperation(LongOperationEvent::Started),
374 ids: vec![],
375 data: Some(id.clone()),
376 });
377 }
378
379 let status = Arc::new(Mutex::new(OperationStatus::Running));
380 let progress = Arc::new(Mutex::new(OperationProgress::new(0.0, None)));
381 let cancel_flag = Arc::new(AtomicBool::new(false));
382
383 let status_clone = status.clone();
384 let progress_clone = progress.clone();
385 let cancel_flag_clone = cancel_flag.clone();
386 let results_clone = self.results.clone();
387 let id_clone = id.clone();
388 let event_hub_opt = self.event_hub.clone();
389 let completion_clone = self.completion.clone();
390
391 let join_handle = thread::spawn(move || {
392 let progress_callback = {
393 let progress = progress_clone.clone();
394 let event_hub_opt = event_hub_opt.clone();
395 let id_for_cb = id_clone.clone();
396 Box::new(move |prog: OperationProgress| {
397 *lock_or_recover(&progress) = prog.clone();
398 if let Some(event_hub) = &event_hub_opt {
399 let payload = serde_json::json!({
400 "id": id_for_cb,
401 "percentage": prog.percentage,
402 "message": prog.message,
403 })
404 .to_string();
405 event_hub.send_event(Event {
406 origin: Origin::LongOperation(LongOperationEvent::Progress),
407 ids: vec![],
408 data: Some(payload),
409 });
410 }
411 }) as Box<dyn Fn(OperationProgress) + Send>
412 };
413
414 // Catch a panic in the operation body so a background failure is
415 // reported as `Failed` (with the panic message) instead of leaving
416 // the operation stuck at `Running` and the thread silently dead.
417 let operation_result =
418 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
419 operation.execute(progress_callback, cancel_flag_clone.clone())
420 })) {
421 Ok(result) => result,
422 Err(payload) => {
423 let msg = payload
424 .downcast_ref::<&str>()
425 .map(|s| s.to_string())
426 .or_else(|| payload.downcast_ref::<String>().cloned())
427 .unwrap_or_else(|| "operation panicked".to_string());
428 Err(anyhow::anyhow!("operation panicked: {msg}"))
429 }
430 };
431
432 let final_status = if cancel_flag_clone.load(Ordering::Relaxed) {
433 OperationStatus::Cancelled
434 } else {
435 match &operation_result {
436 Ok(result) => {
437 // Store the result
438 if let Ok(serialized) = serde_json::to_string(result) {
439 let mut results = lock_or_recover(&results_clone);
440 results.insert(id_clone.clone(), serialized);
441 }
442 OperationStatus::Completed
443 }
444 Err(e) => OperationStatus::Failed(e.to_string()),
445 }
446 };
447
448 // Emit final status event
449 if let Some(event_hub) = &event_hub_opt {
450 let (event, data) = match &final_status {
451 OperationStatus::Completed => (
452 LongOperationEvent::Completed,
453 serde_json::json!({"id": id_clone}).to_string(),
454 ),
455 OperationStatus::Cancelled => (
456 LongOperationEvent::Cancelled,
457 serde_json::json!({"id": id_clone}).to_string(),
458 ),
459 OperationStatus::Failed(err) => (
460 LongOperationEvent::Failed,
461 serde_json::json!({"id": id_clone, "error": err}).to_string(),
462 ),
463 OperationStatus::Running => (
464 LongOperationEvent::Progress,
465 serde_json::json!({"id": id_clone}).to_string(),
466 ),
467 };
468 event_hub.send_event(Event {
469 origin: Origin::LongOperation(event),
470 ids: vec![],
471 data: Some(data),
472 });
473 }
474
475 *lock_or_recover(&status_clone) = final_status;
476
477 // Publish completion **last** — after the result is stored, the
478 // event emitted and the final status written — so any thread woken
479 // by this observes a fully-settled operation rather than racing the
480 // bookkeeping that follows it.
481 completion_clone.mark_finished(&id_clone);
482 });
483
484 let handle = OperationHandle {
485 status,
486 progress,
487 cancel_flag,
488 _join_handle: join_handle,
489 };
490
491 lock_or_recover(&self.operations).insert(id.clone(), Box::new(handle));
492
493 id
494 }
495
496 /// Get the status of an operation
497 pub fn get_operation_status(&self, id: &str) -> Option<OperationStatus> {
498 let operations = lock_or_recover(&self.operations);
499 operations.get(id).map(|handle| handle.get_status())
500 }
501
502 /// Get the progress of an operation
503 pub fn get_operation_progress(&self, id: &str) -> Option<OperationProgress> {
504 let operations = lock_or_recover(&self.operations);
505 operations.get(id).map(|handle| handle.get_progress())
506 }
507
508 /// Cancel an operation
509 pub fn cancel_operation(&self, id: &str) -> bool {
510 let operations = lock_or_recover(&self.operations);
511 if let Some(handle) = operations.get(id) {
512 handle.cancel();
513 // Emit cancelled event immediately
514 if let Some(event_hub) = &self.event_hub {
515 let payload = serde_json::json!({"id": id}).to_string();
516 event_hub.send_event(Event {
517 origin: Origin::LongOperation(LongOperationEvent::Cancelled),
518 ids: vec![],
519 data: Some(payload),
520 });
521 }
522 true
523 } else {
524 false
525 }
526 }
527
528 /// Check if an operation is finished
529 pub fn is_operation_finished(&self, id: &str) -> Option<bool> {
530 let operations = lock_or_recover(&self.operations);
531 operations.get(id).map(|handle| handle.is_finished())
532 }
533
534 /// Remove finished operations from memory
535 pub fn cleanup_finished_operations(&self) {
536 let mut operations = lock_or_recover(&self.operations);
537 operations.retain(|_, handle| !handle.is_finished());
538 }
539
540 /// Get list of all operation IDs
541 pub fn list_operations(&self) -> Vec<String> {
542 let operations = lock_or_recover(&self.operations);
543 operations.keys().cloned().collect()
544 }
545
546 /// Get summary of all operations
547 pub fn get_operations_summary(&self) -> Vec<(String, OperationStatus, OperationProgress)> {
548 let operations = lock_or_recover(&self.operations);
549 operations
550 .iter()
551 .map(|(id, handle)| (id.clone(), handle.get_status(), handle.get_progress()))
552 .collect()
553 }
554
555 /// Store an operation result
556 pub fn store_operation_result<T: serde::Serialize>(&self, id: &str, result: T) -> Result<()> {
557 let serialized = serde_json::to_string(&result)?;
558 let mut results = lock_or_recover(&self.results);
559 results.insert(id.to_string(), serialized);
560 Ok(())
561 }
562
563 /// Get an operation result
564 pub fn get_operation_result(&self, id: &str) -> Option<String> {
565 let results = lock_or_recover(&self.results);
566 results.get(id).cloned()
567 }
568
569 /// A cloneable handle to the completion signal — the supported way to
570 /// **block** until an operation finishes instead of re-checking it on a
571 /// timer.
572 ///
573 /// Handed out as an `Arc` by design. A manager is normally reached through a
574 /// shared lock, and blocking while holding that lock would stall every other
575 /// operation query for as long as the wait lasts. So take the handle, drop
576 /// the manager lock, *then* wait:
577 ///
578 /// ```rust,ignore
579 /// let completion = ctx.long_operation_manager.lock().completion_signal();
580 /// // manager lock released here
581 /// completion.wait_for(&op_id, Some(Duration::from_secs(30)));
582 /// let result = ctx.long_operation_manager.lock().get_operation_result(&op_id);
583 /// ```
584 pub fn completion_signal(&self) -> Arc<OperationCompletion> {
585 Arc::clone(&self.completion)
586 }
587
588 /// Block until `id` finishes; `true` once it has.
589 ///
590 /// Convenience for an owner holding the manager directly. **Do not call this
591 /// through a shared lock** — it blocks for the operation's whole duration and
592 /// would hold that lock the entire time; take
593 /// [`completion_signal`](Self::completion_signal) and wait on that instead.
594 pub fn wait_for_operation(&self, id: &str, timeout: Option<Duration>) -> bool {
595 self.completion.wait_for(id, timeout)
596 }
597}
598
599impl Default for LongOperationManager {
600 fn default() -> Self {
601 Self::new()
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608 use anyhow::anyhow;
609 use std::time::Duration;
610
611 // Example implementation of a long operation
612 pub struct FileProcessingOperation {
613 pub _file_path: String,
614 pub total_files: usize,
615 }
616
617 impl LongOperation for FileProcessingOperation {
618 type Output = ();
619
620 fn execute(
621 &self,
622 progress_callback: Box<dyn Fn(OperationProgress) + Send>,
623 cancel_flag: Arc<AtomicBool>,
624 ) -> Result<Self::Output> {
625 for i in 0..self.total_files {
626 // Check if operation was cancelled
627 if cancel_flag.load(Ordering::Relaxed) {
628 return Err(anyhow!("Operation was cancelled".to_string()));
629 }
630
631 // Simulate work
632 thread::sleep(Duration::from_millis(500));
633
634 // Report progress
635 let percentage = (i as f32 / self.total_files as f32) * 100.0;
636 progress_callback(OperationProgress::new(
637 percentage,
638 Some(format!("Processing file {} of {}", i + 1, self.total_files)),
639 ));
640 }
641
642 // Final progress
643 progress_callback(OperationProgress::new(100.0, Some("Completed".to_string())));
644 Ok(())
645 }
646 }
647
648 /// An operation that finishes at once and returns a value — the shape of a
649 /// small import, and the case a blocking wait must add no latency to.
650 pub struct InstantOperation;
651
652 impl LongOperation for InstantOperation {
653 type Output = u32;
654
655 fn execute(
656 &self,
657 _progress_callback: Box<dyn Fn(OperationProgress) + Send>,
658 _cancel_flag: Arc<AtomicBool>,
659 ) -> Result<Self::Output> {
660 Ok(42)
661 }
662 }
663
664 /// The whole point of the completion signal: a caller is woken the moment the
665 /// worker publishes, with no polling interval and no fixed sleep floor. A
666 /// trivial operation must therefore settle in far less than a timer tick —
667 /// this is what stops N sequential small operations costing N × a poll period.
668 #[test]
669 fn wait_for_returns_as_soon_as_the_operation_finishes() {
670 let manager = LongOperationManager::new();
671 let completion = manager.completion_signal();
672 let started = std::time::Instant::now();
673 let op_id = manager.start_operation(InstantOperation);
674
675 assert!(completion.wait_for(&op_id, Some(Duration::from_secs(5))));
676 let elapsed = started.elapsed();
677
678 // The result is stored *before* completion is published, so a woken
679 // waiter can always read it straight back.
680 assert_eq!(
681 manager.get_operation_result(&op_id).as_deref(),
682 Some("42"),
683 "completion must imply the result is already retrievable"
684 );
685 assert!(
686 elapsed < Duration::from_millis(50),
687 "a trivial operation must not wait out a polling tick (took {elapsed:?})"
688 );
689 }
690
691 /// Waiting on an operation that has already finished returns at once, instead
692 /// of blocking for a signal that was sent before the caller arrived.
693 #[test]
694 fn wait_for_an_already_finished_operation_returns_immediately() {
695 let manager = LongOperationManager::new();
696 let completion = manager.completion_signal();
697 let op_id = manager.start_operation(InstantOperation);
698 assert!(completion.wait_for(&op_id, Some(Duration::from_secs(5))));
699
700 // The finished record persists, so a second wait is instant.
701 let started = std::time::Instant::now();
702 assert!(completion.wait_for(&op_id, Some(Duration::from_secs(5))));
703 assert!(started.elapsed() < Duration::from_millis(50));
704 assert!(completion.is_finished(&op_id));
705 }
706
707 /// The timeout bounds one wait; it is not a deadline for the operation. A
708 /// still-running operation reports `false` and leaves the caller free to wait
709 /// again.
710 #[test]
711 fn wait_for_times_out_while_the_operation_is_still_running() {
712 let manager = LongOperationManager::new();
713 let completion = manager.completion_signal();
714 let op_id = manager.start_operation(FileProcessingOperation {
715 _file_path: "/tmp/test".to_string(),
716 total_files: 5,
717 });
718
719 assert!(
720 !completion.wait_for(&op_id, Some(Duration::from_millis(50))),
721 "a running operation has published no completion"
722 );
723 assert!(!completion.is_finished(&op_id));
724 manager.cancel_operation(&op_id);
725 }
726
727 /// A cancelled operation must still publish completion — otherwise a caller
728 /// blocked on it would be stranded forever.
729 #[test]
730 fn a_cancelled_operation_still_publishes_completion() {
731 let manager = LongOperationManager::new();
732 let completion = manager.completion_signal();
733 let op_id = manager.start_operation(FileProcessingOperation {
734 _file_path: "/tmp/test".to_string(),
735 total_files: 5,
736 });
737 manager.cancel_operation(&op_id);
738
739 assert!(
740 completion.wait_for(&op_id, Some(Duration::from_secs(5))),
741 "cancellation must wake a blocked waiter, not strand it"
742 );
743 }
744
745 #[test]
746 fn test_operation_manager() {
747 let manager = LongOperationManager::new();
748
749 let operation = FileProcessingOperation {
750 _file_path: "/tmp/test".to_string(),
751 total_files: 5,
752 };
753
754 let op_id = manager.start_operation(operation);
755
756 // Check initial status
757 assert_eq!(
758 manager.get_operation_status(&op_id),
759 Some(OperationStatus::Running)
760 );
761
762 // Wait a bit and check progress
763 thread::sleep(Duration::from_millis(100));
764 let progress = manager.get_operation_progress(&op_id);
765 assert!(progress.is_some());
766
767 // Test cancellation
768 assert!(manager.cancel_operation(&op_id));
769 thread::sleep(Duration::from_millis(100));
770 assert_eq!(
771 manager.get_operation_status(&op_id),
772 Some(OperationStatus::Cancelled)
773 );
774 }
775}