gglib_core/download/
completion.rs

1//! Queue run completion tracking types.
2//!
3//! These types represent the completion of an entire queue run, distinct from
4//! individual download completion events. A queue run accumulates all downloads
5//! that complete between idle→busy and busy→idle transitions.
6
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use uuid::Uuid;
10
11use super::types::DownloadId;
12
13/// Stable artifact identity for completion tracking.
14///
15/// This key is computed at enqueue time (before download starts) and remains
16/// stable across retries, failures, and sharded downloads. It represents "what
17/// the user thinks they downloaded" from an artifact perspective, not a request
18/// perspective.
19///
20/// # Identity Semantics
21///
22/// - Same artifact downloaded twice → same key (deduplication)
23/// - All shards in a group → same key (one entry)
24/// - Failures before metadata available → key still valid
25/// - Survives cancellations and retries
26#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(tag = "kind", rename_all = "snake_case")]
28pub enum CompletionKey {
29    /// `HuggingFace` model file.
30    HfFile {
31        /// Repository ID (e.g., "unsloth/Llama-3-GGUF").
32        repo_id: String,
33        /// Git revision (branch, tag, or commit SHA).
34        /// Stores exactly what the user requested (e.g., "main", "v1.0", or a SHA).
35        /// Use "unspecified" if no revision was provided.
36        revision: String,
37        /// Canonical filename (normalized for sharded models).
38        /// Shard suffixes are stripped: "model-00001-of-00008.gguf" → "model.gguf"
39        filename_canon: String,
40        /// Quantization type (e.g., "`Q4_K_M`").
41        /// Optional since some downloads may not have a meaningful quantization.
42        #[serde(skip_serializing_if = "Option::is_none")]
43        quantization: Option<String>,
44    },
45
46    /// File downloaded from URL.
47    UrlFile {
48        /// Source URL.
49        url: String,
50        /// Target filename.
51        filename: String,
52    },
53
54    /// Local file operation.
55    LocalFile {
56        /// Absolute path to the file.
57        path: String,
58    },
59}
60
61impl fmt::Display for CompletionKey {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Self::HfFile {
65                repo_id,
66                quantization,
67                ..
68            } => {
69                if let Some(quant) = quantization {
70                    write!(f, "{repo_id} ({quant})")
71                } else {
72                    write!(f, "{repo_id}")
73                }
74            }
75            Self::UrlFile { filename, .. } => write!(f, "{filename}"),
76            Self::LocalFile { path } => {
77                // Show only filename for local files
78                if let Some(name) = path.rsplit('/').next() {
79                    write!(f, "{name}")
80                } else {
81                    write!(f, "{path}")
82                }
83            }
84        }
85    }
86}
87
88/// Result kind for a completion attempt.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum CompletionKind {
92    /// Successfully downloaded and registered.
93    Downloaded,
94    /// Download failed.
95    Failed,
96    /// Download was cancelled by user.
97    Cancelled,
98    /// File already existed and was validated (not re-downloaded).
99    AlreadyPresent,
100}
101
102/// Counts of attempts by result kind.
103#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
104pub struct AttemptCounts {
105    /// Number of successful downloads.
106    pub downloaded: u32,
107    /// Number of failed attempts.
108    pub failed: u32,
109    /// Number of cancelled attempts.
110    pub cancelled: u32,
111}
112
113impl AttemptCounts {
114    /// Create counts with a single attempt of the given kind.
115    #[must_use]
116    pub const fn from_kind(kind: CompletionKind) -> Self {
117        match kind {
118            CompletionKind::Downloaded => Self {
119                downloaded: 1,
120                failed: 0,
121                cancelled: 0,
122            },
123            CompletionKind::Failed => Self {
124                downloaded: 0,
125                failed: 1,
126                cancelled: 0,
127            },
128            CompletionKind::Cancelled => Self {
129                downloaded: 0,
130                failed: 0,
131                cancelled: 1,
132            },
133            CompletionKind::AlreadyPresent => Self {
134                downloaded: 0,
135                failed: 0,
136                cancelled: 0,
137            },
138        }
139    }
140
141    /// Increment the count for the given kind.
142    pub const fn increment(&mut self, kind: CompletionKind) {
143        match kind {
144            CompletionKind::Downloaded => self.downloaded += 1,
145            CompletionKind::Failed => self.failed += 1,
146            CompletionKind::Cancelled => self.cancelled += 1,
147            CompletionKind::AlreadyPresent => {
148                // AlreadyPresent doesn't increment attempt counts
149                // (it's informational, not a retry)
150            }
151        }
152    }
153
154    /// Total number of attempts across all kinds.
155    #[must_use]
156    pub const fn total(&self) -> u32 {
157        self.downloaded + self.failed + self.cancelled
158    }
159
160    /// Check if there were any retry attempts (more than one total attempt).
161    #[must_use]
162    pub const fn has_retries(&self) -> bool {
163        self.total() > 1
164    }
165}
166
167/// Details for a single completed artifact in a queue run.
168#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
169pub struct CompletionDetail {
170    /// Stable artifact identity key.
171    pub key: CompletionKey,
172    /// Human-readable display name for UI.
173    pub display_name: String,
174    /// Most recent result for this artifact.
175    pub last_result: CompletionKind,
176    /// Unix timestamp (milliseconds since epoch) of last completion.
177    pub last_completed_at_ms: u64,
178    /// All download IDs that contributed to this completion.
179    /// Multiple IDs indicate retries or re-queues.
180    pub download_ids: Vec<DownloadId>,
181    /// Breakdown of attempts by result kind.
182    pub attempt_counts: AttemptCounts,
183}
184
185/// Summary of an entire queue run from start to drain.
186///
187/// Emitted when the queue transitions from busy → idle, capturing all
188/// completions that occurred during the run regardless of timing.
189#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
190pub struct QueueRunSummary {
191    /// Unique identifier for this queue run.
192    pub run_id: Uuid,
193    /// Unix timestamp (milliseconds since epoch) when the run started.
194    pub started_at_ms: u64,
195    /// Unix timestamp (milliseconds since epoch) when the run completed.
196    pub completed_at_ms: u64,
197
198    // Attempt-based totals (diagnostics)
199    /// Total download attempts that succeeded.
200    pub total_attempts_downloaded: u32,
201    /// Total download attempts that failed.
202    pub total_attempts_failed: u32,
203    /// Total download attempts that were cancelled.
204    pub total_attempts_cancelled: u32,
205
206    // Unique key-based totals (UX)
207    /// Number of unique models successfully downloaded.
208    pub unique_models_downloaded: u32,
209    /// Number of unique models that failed.
210    pub unique_models_failed: u32,
211    /// Number of unique models that were cancelled.
212    pub unique_models_cancelled: u32,
213
214    /// True if there are more items than shown in `items`.
215    pub truncated: bool,
216
217    /// Detailed completion records, sorted by `last_completed_at_ms` (newest first).
218    /// Capped at 20 items for payload size management.
219    pub items: Vec<CompletionDetail>,
220}
221
222impl QueueRunSummary {
223    /// Total number of unique models across all result kinds.
224    #[must_use]
225    pub const fn total_unique_models(&self) -> u32 {
226        self.unique_models_downloaded + self.unique_models_failed + self.unique_models_cancelled
227    }
228
229    /// Total number of attempts across all result kinds.
230    #[must_use]
231    pub const fn total_attempts(&self) -> u32 {
232        self.total_attempts_downloaded + self.total_attempts_failed + self.total_attempts_cancelled
233    }
234
235    /// Check if any models had retry attempts.
236    #[must_use]
237    pub fn has_retries(&self) -> bool {
238        self.items
239            .iter()
240            .any(|item| item.attempt_counts.has_retries())
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_completion_key_display() {
250        let key = CompletionKey::HfFile {
251            repo_id: "unsloth/Llama-3-GGUF".to_string(),
252            revision: "main".to_string(),
253            filename_canon: "model.gguf".to_string(),
254            quantization: Some("Q4_K_M".to_string()),
255        };
256        assert_eq!(key.to_string(), "unsloth/Llama-3-GGUF (Q4_K_M)");
257
258        let key_no_quant = CompletionKey::HfFile {
259            repo_id: "unsloth/Llama-3-GGUF".to_string(),
260            revision: "main".to_string(),
261            filename_canon: "model.gguf".to_string(),
262            quantization: None,
263        };
264        assert_eq!(key_no_quant.to_string(), "unsloth/Llama-3-GGUF");
265    }
266
267    #[test]
268    fn test_attempt_counts() {
269        let mut counts = AttemptCounts::from_kind(CompletionKind::Downloaded);
270        assert_eq!(counts.downloaded, 1);
271        assert_eq!(counts.total(), 1);
272        assert!(!counts.has_retries());
273
274        counts.increment(CompletionKind::Failed);
275        assert_eq!(counts.failed, 1);
276        assert_eq!(counts.total(), 2);
277        assert!(counts.has_retries());
278
279        counts.increment(CompletionKind::Downloaded);
280        assert_eq!(counts.downloaded, 2);
281        assert_eq!(counts.total(), 3);
282    }
283
284    #[test]
285    fn test_queue_run_summary_totals() {
286        let summary = QueueRunSummary {
287            run_id: Uuid::nil(),
288            started_at_ms: 0,
289            completed_at_ms: 100_000,
290            total_attempts_downloaded: 5,
291            total_attempts_failed: 1,
292            total_attempts_cancelled: 0,
293            unique_models_downloaded: 3,
294            unique_models_failed: 1,
295            unique_models_cancelled: 0,
296            items: vec![],
297            truncated: false,
298        };
299
300        assert_eq!(summary.total_attempts(), 6);
301        assert_eq!(summary.total_unique_models(), 4);
302    }
303}