Skip to main content

celers_core/
result.rs

1#![allow(
2    clippy::missing_errors_doc,
3    clippy::missing_panics_doc,
4    clippy::missing_fields_in_debug
5)]
6#![allow(clippy::cast_precision_loss)]
7//! `AsyncResult` API for querying task results
8//!
9//! This module provides a Celery-compatible interface for retrieving task results,
10//! checking task state, and waiting for task completion.
11//!
12//! # Example
13//!
14//! ```no_run
15//! use celers_core::{AsyncResult, ResultStore, TaskId, TaskResultValue};
16//! use uuid::Uuid;
17//! use std::time::Duration;
18//! # use async_trait::async_trait;
19//! #
20//! # #[derive(Clone)]
21//! # struct MockBackend;
22//! #
23//! # #[async_trait]
24//! # impl ResultStore for MockBackend {
25//! #     async fn store_result(&self, _: TaskId, _: TaskResultValue) -> celers_core::Result<()> { Ok(()) }
26//! #     async fn get_result(&self, _: TaskId) -> celers_core::Result<Option<TaskResultValue>> { Ok(None) }
27//! #     async fn get_state(&self, _: TaskId) -> celers_core::Result<celers_core::TaskState> { Ok(celers_core::TaskState::Pending) }
28//! #     async fn forget(&self, _: TaskId) -> celers_core::Result<()> { Ok(()) }
29//! #     async fn has_result(&self, _: TaskId) -> celers_core::Result<bool> { Ok(false) }
30//! # }
31//!
32//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
33//! let task_id: TaskId = Uuid::new_v4();
34//! let backend = MockBackend; // Use your actual backend (Redis, Database, etc.)
35//! let result = AsyncResult::new(task_id, backend);
36//!
37//! // Check if the task is ready
38//! if result.ready().await? {
39//!     // Get the result
40//!     if result.successful().await? {
41//!         let value = result.get(Some(Duration::from_secs(30))).await?;
42//!         println!("Task succeeded: {:?}", value);
43//!     } else {
44//!         println!("Task failed");
45//!     }
46//! }
47//! # Ok(())
48//! # }
49//! ```
50
51use crate::state::TaskState;
52use crate::TaskId;
53use async_trait::async_trait;
54use serde_json::Value;
55use std::time::Duration;
56
57/// Result store trait for `AsyncResult` API
58///
59/// This trait provides the storage interface needed by `AsyncResult` for querying
60/// task results in a Celery-compatible way. Implementations should provide
61/// lightweight result storage focused on result state and values.
62#[async_trait]
63pub trait ResultStore: Send + Sync {
64    /// Store a task result
65    async fn store_result(&self, task_id: TaskId, result: TaskResultValue) -> crate::Result<()>;
66
67    /// Retrieve a task result
68    async fn get_result(&self, task_id: TaskId) -> crate::Result<Option<TaskResultValue>>;
69
70    /// Get task state
71    async fn get_state(&self, task_id: TaskId) -> crate::Result<TaskState>;
72
73    /// Delete a task result
74    async fn forget(&self, task_id: TaskId) -> crate::Result<()>;
75
76    /// Check if a result exists
77    async fn has_result(&self, task_id: TaskId) -> crate::Result<bool>;
78
79    // ------------------------------------------------------------------
80    // Result tombstones (default no-op / "unsupported" implementations)
81    //
82    // These DEFAULT methods let a backend distinguish a *forgotten* result
83    // (one with a tombstone marker) from one that simply never existed.
84    // Backends that do not support tombstones inherit the defaults below
85    // unchanged, so existing implementations keep compiling.
86    // ------------------------------------------------------------------
87
88    /// Record a tombstone marking a result as explicitly deleted/forgotten.
89    ///
90    /// The default implementation is a no-op for backends that do not support
91    /// tombstones. Backends that do support them should override this to
92    /// persist the marker so it can later be distinguished from a result that
93    /// never existed.
94    async fn store_tombstone(&self, _tombstone: crate::ResultTombstone) -> crate::Result<()> {
95        Ok(())
96    }
97
98    /// Fetch the tombstone marker for a task, if one was recorded.
99    ///
100    /// The default implementation returns `Ok(None)` ("unsupported / not
101    /// recorded"). Backends that support tombstones should override this.
102    async fn get_tombstone(
103        &self,
104        _task_id: TaskId,
105    ) -> crate::Result<Option<crate::ResultTombstone>> {
106        Ok(None)
107    }
108
109    /// Returns `true` if a tombstone marker is recorded for the task.
110    ///
111    /// The default implementation derives the answer from [`Self::get_tombstone`],
112    /// so backends only need to override `get_tombstone`.
113    async fn has_tombstone(&self, task_id: TaskId) -> crate::Result<bool> {
114        Ok(self.get_tombstone(task_id).await?.is_some())
115    }
116
117    /// Resolve the tri-state existence of a result: present, tombstoned
118    /// (explicitly deleted), or absent (never existed).
119    ///
120    /// The default implementation combines [`Self::has_result`] and
121    /// [`Self::get_tombstone`]: a live result reports
122    /// [`crate::result_tombstone::ResultExistence::Present`], otherwise a
123    /// recorded tombstone reports
124    /// [`crate::result_tombstone::ResultExistence::Tombstoned`], otherwise
125    /// [`crate::result_tombstone::ResultExistence::Absent`]. Because the
126    /// default `get_tombstone` returns `None`, backends without tombstone
127    /// support will only ever distinguish present vs absent — which preserves
128    /// their previous behaviour.
129    async fn result_existence(
130        &self,
131        task_id: TaskId,
132    ) -> crate::Result<crate::result_tombstone::ResultExistence> {
133        if self.has_result(task_id).await? {
134            return Ok(crate::result_tombstone::ResultExistence::Present);
135        }
136        match self.get_tombstone(task_id).await? {
137            Some(tombstone) => Ok(crate::result_tombstone::ResultExistence::Tombstoned(
138                tombstone,
139            )),
140            None => Ok(crate::result_tombstone::ResultExistence::Absent),
141        }
142    }
143
144    /// Forget a result and simultaneously record a tombstone for it.
145    ///
146    /// The default implementation calls [`Self::forget`] followed by
147    /// [`Self::store_tombstone`]. Backends that need this to be atomic should
148    /// override it.
149    async fn forget_with_tombstone(&self, tombstone: crate::ResultTombstone) -> crate::Result<()> {
150        let task_id = tombstone.task_id;
151        self.forget(task_id).await?;
152        self.store_tombstone(tombstone).await
153    }
154
155    // ------------------------------------------------------------------
156    // Per-task-type result TTL (default "unsupported" hook)
157    // ------------------------------------------------------------------
158
159    /// Query the effective result TTL for a task type, given a per-task-type
160    /// TTL configuration.
161    ///
162    /// The default implementation simply resolves the TTL from the supplied
163    /// [`crate::result_ttl::ResultTtlConfig`] (per-task override, else default
164    /// fallback) without touching the backend. Backends that store their own
165    /// per-task TTL policy may override this to consult that policy instead.
166    async fn result_ttl_for(
167        &self,
168        config: &crate::result_ttl::ResultTtlConfig,
169        task_name: &str,
170    ) -> crate::Result<Option<Duration>> {
171        Ok(config.ttl_for(task_name))
172    }
173
174    /// Apply a per-task-type TTL to an already-stored result.
175    ///
176    /// The default implementation is a no-op returning `Ok(false)` to signal
177    /// that the backend did not apply any expiry ("unsupported"). Backends
178    /// with native key-expiry (e.g. Redis) should override this to set the
179    /// expiry derived from `config` for `task_name` and return `Ok(true)`.
180    async fn apply_result_ttl(
181        &self,
182        _task_id: TaskId,
183        _config: &crate::result_ttl::ResultTtlConfig,
184        _task_name: &str,
185    ) -> crate::Result<bool> {
186        Ok(false)
187    }
188}
189
190/// Task result value stored in backend
191#[derive(Debug, Clone)]
192pub enum TaskResultValue {
193    /// Task is pending execution
194    Pending,
195
196    /// Task has been received by worker
197    Received,
198
199    /// Task is currently running
200    Started,
201
202    /// Task completed successfully with result
203    Success(Value),
204
205    /// Task failed with error message and optional traceback
206    Failure {
207        error: String,
208        traceback: Option<String>,
209    },
210
211    /// Task was revoked/cancelled
212    Revoked,
213
214    /// Task is being retried
215    Retry { attempt: u32, max_retries: u32 },
216
217    /// Task was rejected (e.g., validation failed)
218    Rejected { reason: String },
219}
220
221impl TaskResultValue {
222    /// Check if the result is in a terminal state
223    #[inline]
224    #[must_use]
225    pub const fn is_terminal(&self) -> bool {
226        matches!(
227            self,
228            TaskResultValue::Success(_)
229                | TaskResultValue::Failure { .. }
230                | TaskResultValue::Revoked
231                | TaskResultValue::Rejected { .. }
232        )
233    }
234
235    /// Check if the task is pending
236    #[inline]
237    #[must_use]
238    pub const fn is_pending(&self) -> bool {
239        matches!(self, TaskResultValue::Pending)
240    }
241
242    /// Check if the task is ready (in terminal state)
243    #[inline]
244    #[must_use]
245    pub const fn is_ready(&self) -> bool {
246        self.is_terminal()
247    }
248
249    /// Check if the task succeeded
250    #[inline]
251    #[must_use]
252    pub const fn is_successful(&self) -> bool {
253        matches!(self, TaskResultValue::Success(_))
254    }
255
256    /// Check if the task failed
257    #[inline]
258    #[must_use]
259    pub const fn is_failed(&self) -> bool {
260        matches!(
261            self,
262            TaskResultValue::Failure { .. } | TaskResultValue::Rejected { .. }
263        )
264    }
265
266    /// Get the success value if available
267    #[inline]
268    #[must_use]
269    pub fn success_value(&self) -> Option<&Value> {
270        match self {
271            TaskResultValue::Success(v) => Some(v),
272            _ => None,
273        }
274    }
275
276    /// Get the error message if failed
277    #[inline]
278    #[must_use]
279    pub fn error_message(&self) -> Option<&str> {
280        match self {
281            TaskResultValue::Failure { error, .. } => Some(error),
282            TaskResultValue::Rejected { reason } => Some(reason),
283            _ => None,
284        }
285    }
286
287    /// Get the traceback if available
288    #[inline]
289    #[must_use]
290    pub fn traceback(&self) -> Option<&str> {
291        match self {
292            TaskResultValue::Failure { traceback, .. } => traceback.as_deref(),
293            _ => None,
294        }
295    }
296}
297
298/// `AsyncResult` handle for querying task results (Celery-compatible API)
299#[derive(Clone)]
300pub struct AsyncResult<S: ResultStore> {
301    /// Task ID
302    task_id: TaskId,
303
304    /// Result store for retrieving results
305    store: S,
306
307    /// Parent result (for chained tasks)
308    parent: Option<Box<AsyncResult<S>>>,
309
310    /// Child results (for group/chord tasks)
311    children: Vec<AsyncResult<S>>,
312}
313
314impl<S: ResultStore + Clone> AsyncResult<S> {
315    /// Create a new `AsyncResult` for a task
316    pub fn new(task_id: TaskId, store: S) -> Self {
317        Self {
318            task_id,
319            store,
320            parent: None,
321            children: Vec::new(),
322        }
323    }
324
325    /// Create an `AsyncResult` with a parent
326    pub fn with_parent(task_id: TaskId, store: S, parent: AsyncResult<S>) -> Self {
327        Self {
328            task_id,
329            store,
330            parent: Some(Box::new(parent)),
331            children: Vec::new(),
332        }
333    }
334
335    /// Create an `AsyncResult` with children (for group/chord results)
336    pub fn with_children(task_id: TaskId, store: S, children: Vec<AsyncResult<S>>) -> Self {
337        Self {
338            task_id,
339            store,
340            parent: None,
341            children,
342        }
343    }
344
345    /// Get the task ID
346    #[inline]
347    #[must_use]
348    pub fn task_id(&self) -> TaskId {
349        self.task_id
350    }
351
352    /// Get the parent result if this is a linked task
353    #[inline]
354    #[must_use]
355    pub fn parent(&self) -> Option<&AsyncResult<S>> {
356        self.parent.as_deref()
357    }
358
359    /// Get child results (for group/chord tasks)
360    #[inline]
361    #[must_use]
362    pub fn children(&self) -> &[AsyncResult<S>] {
363        &self.children
364    }
365
366    /// Add a child result
367    pub fn add_child(&mut self, child: AsyncResult<S>) {
368        self.children.push(child);
369    }
370
371    /// Check if all children are ready (completed)
372    pub async fn children_ready(&self) -> crate::Result<bool> {
373        for child in &self.children {
374            if !child.ready().await? {
375                return Ok(false);
376            }
377        }
378        Ok(true)
379    }
380
381    /// Get results from all children
382    ///
383    /// Returns a vector of results in the same order as children were added.
384    /// Returns an error if any child failed.
385    pub async fn collect_children(
386        &self,
387        timeout: Option<Duration>,
388    ) -> crate::Result<Vec<Option<Value>>> {
389        let mut results = Vec::with_capacity(self.children.len());
390        for child in &self.children {
391            results.push(child.get(timeout).await?);
392        }
393        Ok(results)
394    }
395
396    /// Check if the task is ready (in terminal state)
397    pub async fn ready(&self) -> crate::Result<bool> {
398        let state = self.store.get_state(self.task_id).await?;
399        Ok(state.is_terminal())
400    }
401
402    /// Check if the task completed successfully
403    pub async fn successful(&self) -> crate::Result<bool> {
404        match self.store.get_result(self.task_id).await? {
405            Some(result) => Ok(result.is_successful()),
406            None => Ok(false),
407        }
408    }
409
410    /// Check if the task failed
411    pub async fn failed(&self) -> crate::Result<bool> {
412        match self.store.get_result(self.task_id).await? {
413            Some(result) => Ok(result.is_failed()),
414            None => Ok(false),
415        }
416    }
417
418    /// Get the current task state
419    pub async fn state(&self) -> crate::Result<TaskState> {
420        self.store.get_state(self.task_id).await
421    }
422
423    /// Get task information/metadata
424    pub async fn info(&self) -> crate::Result<Option<TaskResultValue>> {
425        self.store.get_result(self.task_id).await
426    }
427
428    /// Get the result, blocking until it's ready
429    ///
430    /// # Arguments
431    /// * `timeout` - Optional timeout duration. If None, waits indefinitely.
432    ///
433    /// # Returns
434    /// * `Ok(Some(Value))` - Task succeeded with result
435    /// * `Ok(None)` - Task completed but has no result
436    /// * `Err(_)` - Task failed or timeout occurred
437    pub async fn get(&self, timeout: Option<Duration>) -> crate::Result<Option<Value>> {
438        let start = std::time::Instant::now();
439        let poll_interval = Duration::from_millis(100);
440
441        loop {
442            // Check if timeout expired
443            if let Some(timeout_duration) = timeout {
444                if start.elapsed() > timeout_duration {
445                    return Err(crate::CelersError::Timeout(format!(
446                        "Task {} did not complete within {:?}",
447                        self.task_id, timeout_duration
448                    )));
449                }
450            }
451
452            // Get current result
453            if let Some(result) = self.store.get_result(self.task_id).await? {
454                match result {
455                    TaskResultValue::Success(value) => return Ok(Some(value)),
456                    TaskResultValue::Failure { error, traceback } => {
457                        let msg = if let Some(tb) = traceback {
458                            format!("{error}\n{tb}")
459                        } else {
460                            error
461                        };
462                        return Err(crate::CelersError::TaskExecution(msg));
463                    }
464                    TaskResultValue::Revoked => {
465                        return Err(crate::CelersError::TaskRevoked(self.task_id));
466                    }
467                    TaskResultValue::Rejected { reason } => {
468                        return Err(crate::CelersError::TaskExecution(format!(
469                            "Task rejected: {reason}"
470                        )));
471                    }
472                    // Task not ready yet, continue polling
473                    _ => {}
474                }
475            } else {
476                // Result not yet available
477            }
478
479            // Wait before next poll
480            tokio::time::sleep(poll_interval).await;
481        }
482    }
483
484    /// Get the result without blocking
485    ///
486    /// Returns None if the task is not yet complete
487    pub async fn result(&self) -> crate::Result<Option<Value>> {
488        match self.store.get_result(self.task_id).await? {
489            Some(TaskResultValue::Success(value)) => Ok(Some(value)),
490            _ => Ok(None),
491        }
492    }
493
494    /// Get the error traceback if the task failed
495    pub async fn traceback(&self) -> crate::Result<Option<String>> {
496        match self.store.get_result(self.task_id).await? {
497            Some(result) => Ok(result.traceback().map(String::from)),
498            None => Ok(None),
499        }
500    }
501
502    /// Revoke the task
503    pub async fn revoke(&self) -> crate::Result<()> {
504        self.store
505            .store_result(self.task_id, TaskResultValue::Revoked)
506            .await
507    }
508
509    /// Forget the task result (delete from store)
510    pub async fn forget(&self) -> crate::Result<()> {
511        self.store.forget(self.task_id).await
512    }
513
514    /// Wait for the task to complete and return the result
515    ///
516    /// This is a convenience method that combines `ready()` and `get()`
517    pub async fn wait(&self, timeout: Option<Duration>) -> crate::Result<Value> {
518        match self.get(timeout).await? {
519            Some(value) => Ok(value),
520            None => Err(crate::CelersError::TaskExecution(
521                "Task completed but returned no value".to_string(),
522            )),
523        }
524    }
525}
526
527impl<S: ResultStore + Clone> std::fmt::Debug for AsyncResult<S> {
528    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529        f.debug_struct("AsyncResult")
530            .field("store", &"<ResultStore>")
531            .field("task_id", &self.task_id)
532            .field("has_parent", &self.parent.is_some())
533            .field("num_children", &self.children.len())
534            .finish()
535    }
536}
537
538impl<S: ResultStore + Clone> std::fmt::Display for AsyncResult<S> {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        write!(f, "AsyncResult[{}]", &self.task_id.to_string()[..8])
541    }
542}
543
544// ============================================================================
545// Advanced Result Features
546// ============================================================================
547
548use chrono::{DateTime, Utc};
549use serde::{Deserialize, Serialize};
550use std::collections::HashMap;
551
552/// Result metadata for storing additional information with task results
553#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct ResultMetadata {
555    /// Custom tags for categorization
556    pub tags: Vec<String>,
557
558    /// Custom key-value fields
559    pub custom_fields: HashMap<String, Value>,
560
561    /// Result creation timestamp
562    pub created_at: DateTime<Utc>,
563
564    /// Result expiration timestamp (TTL)
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub expires_at: Option<DateTime<Utc>>,
567
568    /// Whether the result is compressed
569    pub compressed: bool,
570
571    /// Compression algorithm used (if compressed)
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub compression_algorithm: Option<String>,
574
575    /// Whether the result is chunked
576    pub chunked: bool,
577
578    /// Total number of chunks (if chunked)
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub total_chunks: Option<usize>,
581
582    /// Original size in bytes (before compression/chunking)
583    #[serde(skip_serializing_if = "Option::is_none")]
584    pub original_size: Option<usize>,
585
586    /// Compressed size in bytes
587    #[serde(skip_serializing_if = "Option::is_none")]
588    pub compressed_size: Option<usize>,
589}
590
591impl ResultMetadata {
592    /// Create new result metadata
593    #[must_use]
594    pub fn new() -> Self {
595        Self {
596            tags: Vec::new(),
597            custom_fields: HashMap::new(),
598            created_at: Utc::now(),
599            expires_at: None,
600            compressed: false,
601            compression_algorithm: None,
602            chunked: false,
603            total_chunks: None,
604            original_size: None,
605            compressed_size: None,
606        }
607    }
608
609    /// Add a tag
610    #[must_use]
611    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
612        self.tags.push(tag.into());
613        self
614    }
615
616    /// Add multiple tags
617    #[must_use]
618    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
619        self.tags.extend(tags);
620        self
621    }
622
623    /// Add a custom field
624    #[must_use]
625    pub fn with_field(mut self, key: impl Into<String>, value: Value) -> Self {
626        self.custom_fields.insert(key.into(), value);
627        self
628    }
629
630    ///
631    /// # Panics
632    ///
633    /// Panics if the TTL duration cannot be converted to a chrono duration.
634    /// Set TTL (time to live)
635    #[must_use]
636    pub fn with_ttl(mut self, ttl: Duration) -> Self {
637        self.expires_at = Some(
638            Utc::now() + chrono::Duration::from_std(ttl).expect("TTL duration should be valid"),
639        );
640        self
641    }
642
643    /// Set expiration timestamp
644    #[must_use]
645    pub fn with_expires_at(mut self, expires_at: DateTime<Utc>) -> Self {
646        self.expires_at = Some(expires_at);
647        self
648    }
649
650    /// Check if the result has expired
651    #[inline]
652    #[must_use]
653    pub fn is_expired(&self) -> bool {
654        self.expires_at.is_some_and(|exp| Utc::now() > exp)
655    }
656
657    /// Get time until expiration
658    #[inline]
659    #[must_use]
660    pub fn time_until_expiration(&self) -> Option<Duration> {
661        self.expires_at.and_then(|exp| {
662            let diff = exp - Utc::now();
663            diff.to_std().ok()
664        })
665    }
666
667    /// Mark as compressed
668    #[must_use]
669    pub fn with_compression(
670        mut self,
671        algorithm: impl Into<String>,
672        original_size: usize,
673        compressed_size: usize,
674    ) -> Self {
675        self.compressed = true;
676        self.compression_algorithm = Some(algorithm.into());
677        self.original_size = Some(original_size);
678        self.compressed_size = Some(compressed_size);
679        self
680    }
681
682    /// Mark as chunked
683    #[must_use]
684    pub fn with_chunking(mut self, total_chunks: usize) -> Self {
685        self.chunked = true;
686        self.total_chunks = Some(total_chunks);
687        self
688    }
689
690    /// Get compression ratio
691    #[allow(clippy::cast_precision_loss)]
692    #[must_use]
693    pub fn compression_ratio(&self) -> Option<f64> {
694        if let (Some(orig), Some(comp)) = (self.original_size, self.compressed_size) {
695            if orig > 0 {
696                return Some(comp as f64 / orig as f64);
697            }
698        }
699        None
700    }
701}
702
703impl Default for ResultMetadata {
704    fn default() -> Self {
705        Self::new()
706    }
707}
708
709/// Result chunk for large results
710#[derive(Debug, Clone, Serialize, Deserialize)]
711pub struct ResultChunk {
712    /// Chunk index (0-based)
713    pub index: usize,
714
715    /// Total number of chunks
716    pub total: usize,
717
718    /// Chunk data
719    pub data: Vec<u8>,
720
721    /// Checksum for integrity verification
722    #[serde(skip_serializing_if = "Option::is_none")]
723    pub checksum: Option<String>,
724}
725
726impl ResultChunk {
727    /// Create a new result chunk
728    #[must_use]
729    pub fn new(index: usize, total: usize, data: Vec<u8>) -> Self {
730        Self {
731            index,
732            total,
733            data,
734            checksum: None,
735        }
736    }
737
738    /// Add checksum
739    #[must_use]
740    pub fn with_checksum(mut self, checksum: impl Into<String>) -> Self {
741        self.checksum = Some(checksum.into());
742        self
743    }
744
745    /// Check if this is the last chunk
746    #[inline]
747    #[must_use]
748    pub const fn is_last(&self) -> bool {
749        self.index == self.total - 1
750    }
751}
752
753/// Result tombstone marker for deleted tasks
754#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
755pub struct ResultTombstone {
756    /// Task ID
757    pub task_id: TaskId,
758
759    /// Deletion timestamp
760    pub deleted_at: DateTime<Utc>,
761
762    /// Reason for deletion
763    #[serde(skip_serializing_if = "Option::is_none")]
764    pub reason: Option<String>,
765
766    /// Who deleted it (user, system, etc.)
767    #[serde(skip_serializing_if = "Option::is_none")]
768    pub deleted_by: Option<String>,
769
770    /// TTL for the tombstone itself
771    #[serde(skip_serializing_if = "Option::is_none")]
772    pub tombstone_ttl: Option<Duration>,
773}
774
775impl ResultTombstone {
776    /// Create a new tombstone
777    #[must_use]
778    pub fn new(task_id: TaskId) -> Self {
779        Self {
780            task_id,
781            deleted_at: Utc::now(),
782            reason: None,
783            deleted_by: None,
784            tombstone_ttl: None,
785        }
786    }
787
788    /// Set deletion reason
789    #[must_use]
790    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
791        self.reason = Some(reason.into());
792        self
793    }
794
795    /// Set who deleted it
796    #[must_use]
797    pub fn with_deleted_by(mut self, deleted_by: impl Into<String>) -> Self {
798        self.deleted_by = Some(deleted_by.into());
799        self
800    }
801
802    /// Set tombstone TTL
803    #[must_use]
804    pub fn with_ttl(mut self, ttl: Duration) -> Self {
805        self.tombstone_ttl = Some(ttl);
806        self
807    }
808}
809
810/// Extended result store with advanced features
811#[async_trait]
812pub trait ExtendedResultStore: ResultStore {
813    /// Store result with metadata
814    async fn store_result_with_metadata(
815        &self,
816        task_id: TaskId,
817        result: TaskResultValue,
818        metadata: ResultMetadata,
819    ) -> crate::Result<()>;
820
821    /// Get result metadata
822    async fn get_metadata(&self, task_id: TaskId) -> crate::Result<Option<ResultMetadata>>;
823
824    /// Store a result chunk
825    async fn store_chunk(&self, task_id: TaskId, chunk: ResultChunk) -> crate::Result<()>;
826
827    /// Get a result chunk
828    async fn get_chunk(&self, task_id: TaskId, index: usize) -> crate::Result<Option<ResultChunk>>;
829
830    /// Get all chunks for a task
831    async fn get_all_chunks(&self, task_id: TaskId) -> crate::Result<Vec<ResultChunk>>;
832
833    /// Store a tombstone
834    async fn store_tombstone(&self, tombstone: ResultTombstone) -> crate::Result<()>;
835
836    /// Get a tombstone
837    async fn get_tombstone(&self, task_id: TaskId) -> crate::Result<Option<ResultTombstone>>;
838
839    /// Check if a task has a tombstone
840    async fn has_tombstone(&self, task_id: TaskId) -> crate::Result<bool> {
841        // Disambiguate from the `ResultStore::get_tombstone` default method,
842        // which is also in scope because `ExtendedResultStore: ResultStore`.
843        Ok(ExtendedResultStore::get_tombstone(self, task_id)
844            .await?
845            .is_some())
846    }
847
848    /// Cleanup expired results
849    async fn cleanup_expired(&self) -> crate::Result<usize>;
850
851    /// Query results by tags
852    async fn query_by_tags(&self, tags: &[String]) -> crate::Result<Vec<TaskId>>;
853}
854
855/// Compression helper for result values
856pub struct ResultCompressor {
857    threshold_bytes: usize,
858}
859
860impl ResultCompressor {
861    /// Create a new compressor with threshold
862    #[must_use]
863    pub fn new(threshold_bytes: usize) -> Self {
864        Self { threshold_bytes }
865    }
866
867    /// Check if value should be compressed
868    #[must_use]
869    pub const fn should_compress(&self, data: &[u8]) -> bool {
870        data.len() >= self.threshold_bytes
871    }
872
873    /// Compress data (to be implemented by backend-specific compressors)
874    ///
875    /// Note: Actual compression implementations are provided by backend crates
876    /// (e.g., celers-backend-redis) which have the compression dependencies.
877    pub fn compress(&self, _data: &[u8], _algorithm: &str) -> crate::Result<Vec<u8>> {
878        Err(crate::CelersError::Other(
879            "Compression not available - use backend-specific implementation".to_string(),
880        ))
881    }
882
883    /// Decompress data (to be implemented by backend-specific compressors)
884    ///
885    /// Note: Actual decompression implementations are provided by backend crates
886    /// (e.g., celers-backend-redis) which have the compression dependencies.
887    pub fn decompress(&self, _data: &[u8], _algorithm: &str) -> crate::Result<Vec<u8>> {
888        Err(crate::CelersError::Other(
889            "Decompression not available - use backend-specific implementation".to_string(),
890        ))
891    }
892}
893
894impl Default for ResultCompressor {
895    fn default() -> Self {
896        Self::new(1024 * 1024) // 1MB threshold
897    }
898}
899
900/// Chunker for large results
901pub struct ResultChunker {
902    chunk_size: usize,
903}
904
905impl ResultChunker {
906    /// Create a new chunker
907    #[must_use]
908    pub fn new(chunk_size: usize) -> Self {
909        Self { chunk_size }
910    }
911
912    /// Split data into chunks
913    #[must_use]
914    pub fn chunk(&self, data: &[u8]) -> Vec<ResultChunk> {
915        let total = data.len().div_ceil(self.chunk_size);
916
917        data.chunks(self.chunk_size)
918            .enumerate()
919            .map(|(index, chunk)| ResultChunk::new(index, total, chunk.to_vec()))
920            .collect()
921    }
922
923    /// Reassemble chunks into original data
924    pub fn reassemble(&self, chunks: &[ResultChunk]) -> crate::Result<Vec<u8>> {
925        if chunks.is_empty() {
926            return Ok(Vec::new());
927        }
928
929        // Verify chunks are complete and in order
930        let total = chunks[0].total;
931        if chunks.len() != total {
932            return Err(crate::CelersError::Other(format!(
933                "Incomplete chunks: expected {}, got {}",
934                total,
935                chunks.len()
936            )));
937        }
938
939        let mut result = Vec::new();
940        for (i, chunk) in chunks.iter().enumerate() {
941            if chunk.index != i {
942                return Err(crate::CelersError::Other(format!(
943                    "Chunk out of order: expected index {}, got {}",
944                    i, chunk.index
945                )));
946            }
947            result.extend_from_slice(&chunk.data);
948        }
949
950        Ok(result)
951    }
952}
953
954impl Default for ResultChunker {
955    fn default() -> Self {
956        Self::new(256 * 1024) // 256KB chunks
957    }
958}
959
960#[cfg(test)]
961mod tests {
962    use super::*;
963    use std::collections::HashMap;
964    use std::sync::{Arc, Mutex};
965    use uuid::Uuid;
966
967    // Mock backend for testing
968    #[derive(Clone)]
969    struct MockBackend {
970        results: Arc<Mutex<HashMap<TaskId, TaskResultValue>>>,
971        states: Arc<Mutex<HashMap<TaskId, TaskState>>>,
972    }
973
974    impl MockBackend {
975        fn new() -> Self {
976            Self {
977                results: Arc::new(Mutex::new(HashMap::new())),
978                states: Arc::new(Mutex::new(HashMap::new())),
979            }
980        }
981
982        fn set_result(&self, task_id: TaskId, result: TaskResultValue, state: TaskState) {
983            self.results
984                .lock()
985                .unwrap_or_else(|e| e.into_inner())
986                .insert(task_id, result);
987            self.states
988                .lock()
989                .unwrap_or_else(|e| e.into_inner())
990                .insert(task_id, state);
991        }
992    }
993
994    #[async_trait]
995    impl ResultStore for MockBackend {
996        async fn store_result(
997            &self,
998            task_id: TaskId,
999            result: TaskResultValue,
1000        ) -> crate::Result<()> {
1001            self.results
1002                .lock()
1003                .unwrap_or_else(|e| e.into_inner())
1004                .insert(task_id, result);
1005            Ok(())
1006        }
1007
1008        async fn get_result(&self, task_id: TaskId) -> crate::Result<Option<TaskResultValue>> {
1009            Ok(self
1010                .results
1011                .lock()
1012                .unwrap_or_else(|e| e.into_inner())
1013                .get(&task_id)
1014                .cloned())
1015        }
1016
1017        async fn get_state(&self, task_id: TaskId) -> crate::Result<TaskState> {
1018            Ok(self
1019                .states
1020                .lock()
1021                .unwrap_or_else(|e| e.into_inner())
1022                .get(&task_id)
1023                .cloned()
1024                .unwrap_or(TaskState::Pending))
1025        }
1026
1027        async fn forget(&self, task_id: TaskId) -> crate::Result<()> {
1028            self.results
1029                .lock()
1030                .unwrap_or_else(|e| e.into_inner())
1031                .remove(&task_id);
1032            self.states
1033                .lock()
1034                .unwrap_or_else(|e| e.into_inner())
1035                .remove(&task_id);
1036            Ok(())
1037        }
1038
1039        async fn has_result(&self, task_id: TaskId) -> crate::Result<bool> {
1040            Ok(self
1041                .results
1042                .lock()
1043                .unwrap_or_else(|e| e.into_inner())
1044                .contains_key(&task_id))
1045        }
1046    }
1047
1048    #[tokio::test]
1049    async fn test_async_result_ready() {
1050        let backend = MockBackend::new();
1051        let task_id = Uuid::new_v4();
1052
1053        backend.set_result(
1054            task_id,
1055            TaskResultValue::Success(Value::String("test".to_string())),
1056            TaskState::Succeeded(vec![]),
1057        );
1058
1059        let result = AsyncResult::new(task_id, backend);
1060        assert!(result.ready().await.unwrap());
1061    }
1062
1063    #[tokio::test]
1064    async fn test_async_result_successful() {
1065        let backend = MockBackend::new();
1066        let task_id = Uuid::new_v4();
1067
1068        backend.set_result(
1069            task_id,
1070            TaskResultValue::Success(Value::String("test".to_string())),
1071            TaskState::Succeeded(vec![]),
1072        );
1073
1074        let result = AsyncResult::new(task_id, backend);
1075        assert!(result.successful().await.unwrap());
1076        assert!(!result.failed().await.unwrap());
1077    }
1078
1079    #[tokio::test]
1080    async fn test_async_result_failed() {
1081        let backend = MockBackend::new();
1082        let task_id = Uuid::new_v4();
1083
1084        backend.set_result(
1085            task_id,
1086            TaskResultValue::Failure {
1087                error: "Test error".to_string(),
1088                traceback: None,
1089            },
1090            TaskState::Failed(String::from("Test error")),
1091        );
1092
1093        let result = AsyncResult::new(task_id, backend);
1094        assert!(result.failed().await.unwrap());
1095        assert!(!result.successful().await.unwrap());
1096    }
1097
1098    #[tokio::test]
1099    async fn test_async_result_get_success() {
1100        let backend = MockBackend::new();
1101        let task_id = Uuid::new_v4();
1102
1103        backend.set_result(
1104            task_id,
1105            TaskResultValue::Success(Value::String("success".to_string())),
1106            TaskState::Succeeded(vec![]),
1107        );
1108
1109        let result = AsyncResult::new(task_id, backend);
1110        let value = result.get(Some(Duration::from_secs(1))).await.unwrap();
1111        assert_eq!(value, Some(Value::String("success".to_string())));
1112    }
1113
1114    #[tokio::test]
1115    async fn test_async_result_forget() {
1116        let backend = MockBackend::new();
1117        let task_id = Uuid::new_v4();
1118
1119        backend.set_result(
1120            task_id,
1121            TaskResultValue::Success(Value::String("test".to_string())),
1122            TaskState::Succeeded(vec![]),
1123        );
1124
1125        let result = AsyncResult::new(task_id, backend.clone());
1126        assert!(backend.has_result(task_id).await.unwrap());
1127
1128        result.forget().await.unwrap();
1129        assert!(!backend.has_result(task_id).await.unwrap());
1130    }
1131
1132    #[tokio::test]
1133    async fn test_async_result_children() {
1134        let backend = MockBackend::new();
1135
1136        // Create parent task
1137        let parent_id = Uuid::new_v4();
1138        backend.set_result(
1139            parent_id,
1140            TaskResultValue::Success(Value::String("parent".to_string())),
1141            TaskState::Succeeded(vec![]),
1142        );
1143
1144        // Create child tasks
1145        let child1_id = Uuid::new_v4();
1146        let child2_id = Uuid::new_v4();
1147        backend.set_result(
1148            child1_id,
1149            TaskResultValue::Success(Value::Number(serde_json::Number::from(1))),
1150            TaskState::Succeeded(vec![]),
1151        );
1152        backend.set_result(
1153            child2_id,
1154            TaskResultValue::Success(Value::Number(serde_json::Number::from(2))),
1155            TaskState::Succeeded(vec![]),
1156        );
1157
1158        // Create child AsyncResults
1159        let child1 = AsyncResult::new(child1_id, backend.clone());
1160        let child2 = AsyncResult::new(child2_id, backend.clone());
1161
1162        // Create parent with children
1163        let parent = AsyncResult::with_children(parent_id, backend, vec![child1, child2]);
1164
1165        // Test children access
1166        assert_eq!(parent.children().len(), 2);
1167        assert_eq!(parent.children()[0].task_id(), child1_id);
1168        assert_eq!(parent.children()[1].task_id(), child2_id);
1169
1170        // Test children_ready
1171        assert!(parent.children_ready().await.unwrap());
1172
1173        // Test collect_children
1174        let results = parent
1175            .collect_children(Some(Duration::from_secs(1)))
1176            .await
1177            .unwrap();
1178        assert_eq!(results.len(), 2);
1179        assert_eq!(results[0], Some(Value::Number(serde_json::Number::from(1))));
1180        assert_eq!(results[1], Some(Value::Number(serde_json::Number::from(2))));
1181    }
1182
1183    #[tokio::test]
1184    async fn test_async_result_add_child() {
1185        let backend = MockBackend::new();
1186
1187        let parent_id = Uuid::new_v4();
1188        let child_id = Uuid::new_v4();
1189
1190        backend.set_result(
1191            child_id,
1192            TaskResultValue::Success(Value::String("child".to_string())),
1193            TaskState::Succeeded(vec![]),
1194        );
1195
1196        let mut parent = AsyncResult::new(parent_id, backend.clone());
1197        assert_eq!(parent.children().len(), 0);
1198
1199        let child = AsyncResult::new(child_id, backend);
1200        parent.add_child(child);
1201
1202        assert_eq!(parent.children().len(), 1);
1203        assert_eq!(parent.children()[0].task_id(), child_id);
1204    }
1205}