ai_batch_queue/lib.rs
1//! # AI Batch Queue
2//!
3//! Model-aware batch processing queue with ETA estimation for Tauri applications.
4//!
5//! ## Key Features
6//!
7//! - **Resource-aware reordering** — automatically groups jobs by resource key
8//! (e.g. model name) to minimize expensive swaps
9//! - **Size-bucketed ETA estimation** — tracks processing durations by
10//! (resource, operation, size) for accurate time predictions
11//! - **Item-level status tracking** — each item has its own lifecycle
12//! - **Overwrite policies** — skip items that already have results
13//! - **Progressive completion with retry** — failed items can be retried
14//! without re-processing successful ones
15//! - **Trace context propagation** — optional `stack-ids` `TraceCtx`,
16//! `AttemptId`, and `TrialId` on each batch item for cross-crate
17//! observability and retry lineage tracking
18//!
19//! ## Quick Start
20//!
21//! 1. Define your item data type
22//! 2. Implement [`BatchItemHandler`] for your processing logic
23//! 3. Create a [`BatchQueue`] and register it in Tauri state
24//! 4. Call [`executor::spawn()`] to start the background processor
25
26pub mod eta;
27pub mod executor;
28pub mod queue;
29pub mod types;
30
31pub use queue::BatchQueue;
32pub use types::{
33 BatchCompletionSummary, BatchItem, BatchItemStatus, BatchJob, BatchJobStatus, EtaConfidence,
34 EtaEstimate, ItemResult, OverwritePolicy, SchedulingConfig, SizeBucket,
35};
36
37/// Trait for processing individual items in a batch.
38///
39/// Implement this for your application to define:
40/// - How to process each item (`process`)
41/// - Whether an item should be skipped (`should_skip`)
42///
43/// # Type Parameter
44///
45/// `D` is the per-item data type (e.g. a file path, image reference, document ID).
46///
47/// # Example
48///
49/// ```ignore
50/// use ai_batch_queue::*;
51///
52/// struct MyProcessor;
53///
54/// impl BatchItemHandler<String> for MyProcessor {
55/// async fn process(
56/// &self,
57/// data: &String,
58/// resource_key: &str,
59/// operation: &str,
60/// ) -> anyhow::Result<ItemResult> {
61/// println!("Processing {} with {}", data, resource_key);
62/// Ok(ItemResult::success())
63/// }
64///
65/// fn should_skip(&self, data: &String, operation: &str) -> bool {
66/// false // never skip
67/// }
68/// }
69/// ```
70pub trait BatchItemHandler<D>: Send + Sync + 'static
71where
72 D: Clone + Send + Sync + serde::Serialize,
73{
74 /// Process a single item.
75 ///
76 /// # Arguments
77 /// * `data` — the item's user-defined data payload
78 /// * `resource_key` — the resource this batch uses (e.g. model name)
79 /// * `operation` — the operation label (e.g. "tag", "caption")
80 fn process(
81 &self,
82 data: &D,
83 resource_key: &str,
84 operation: &str,
85 ) -> impl std::future::Future<Output = anyhow::Result<ItemResult>> + Send;
86
87 /// Check if this item should be skipped when the overwrite policy is `Skip`.
88 ///
89 /// Return `true` to skip (item already has results).
90 /// Default implementation never skips.
91 fn should_skip(&self, _data: &D, _operation: &str) -> bool {
92 false
93 }
94}
95
96/// Trait for persisting batch queue state.
97///
98/// The default [`BatchQueue`] is in-memory only. Implement this trait to
99/// persist jobs across restarts (e.g., to SQLite or a file).
100pub trait BatchStore<D>: Send + Sync
101where
102 D: Clone + Send + Sync + serde::Serialize + serde::de::DeserializeOwned + 'static,
103{
104 /// Save or update a batch job.
105 fn save_job(&self, job: &BatchJob<D>) -> anyhow::Result<()>;
106
107 /// Load all jobs (for startup recovery).
108 fn load_all(&self) -> anyhow::Result<Vec<BatchJob<D>>>;
109
110 /// Delete a completed/cancelled job.
111 fn delete_job(&self, job_id: &str) -> anyhow::Result<()>;
112}
113
114/// Helper to build a [`BatchJob`] from a list of items.
115///
116/// Trace fields (`trace_ctx`, `attempt_id`, `trial_id`) are initialized to
117/// `None`. Callers that need trace propagation should set them after
118/// construction or use [`build_job_traced`].
119///
120/// # Example
121///
122/// ```
123/// use ai_batch_queue::*;
124///
125/// let job = build_job(
126/// "llava:13b",
127/// "tag",
128/// OverwritePolicy::Skip,
129/// vec![
130/// ("img-1".to_string(), "path/to/1.png".to_string(), SizeBucket::Medium),
131/// ("img-2".to_string(), "path/to/2.png".to_string(), SizeBucket::Large),
132/// ],
133/// );
134///
135/// assert_eq!(job.items.len(), 2);
136/// assert_eq!(job.resource_key, "llava:13b");
137/// ```
138pub fn build_job<D>(
139 resource_key: &str,
140 operation: &str,
141 overwrite_policy: OverwritePolicy,
142 items: Vec<(String, D, SizeBucket)>,
143) -> BatchJob<D>
144where
145 D: Clone + Send + Sync + serde::Serialize,
146{
147 let batch_items = items
148 .into_iter()
149 .map(|(id, data, bucket)| BatchItem {
150 id,
151 data,
152 status: BatchItemStatus::Pending,
153 error: None,
154 duration_ms: None,
155 size_bucket: bucket,
156 trace_ctx: None,
157 attempt_id: None,
158 trial_id: None,
159 })
160 .collect();
161
162 BatchJob {
163 id: String::new(),
164 resource_key: resource_key.to_string(),
165 operation: operation.to_string(),
166 overwrite_policy,
167 items: batch_items,
168 status: BatchJobStatus::Queued,
169 created_at: String::new(),
170 started_at: None,
171 completed_at: None,
172 reordered: false,
173 reorder_note: None,
174 }
175}
176
177/// Helper to build a [`BatchJob`] with trace context propagation.
178///
179/// Each item receives the provided `trace_ctx` and a freshly generated
180/// `AttemptId` (one per item, since each item is its own retry-owner
181/// boundary within the batch). `TrialId` is left `None` — it will be
182/// stamped by the executor on each concrete execution.
183pub fn build_job_traced<D>(
184 resource_key: &str,
185 operation: &str,
186 overwrite_policy: OverwritePolicy,
187 items: Vec<(String, D, SizeBucket)>,
188 trace_ctx: stack_ids::TraceCtx,
189) -> BatchJob<D>
190where
191 D: Clone + Send + Sync + serde::Serialize,
192{
193 let batch_items = items
194 .into_iter()
195 .map(|(id, data, bucket)| BatchItem {
196 id,
197 data,
198 status: BatchItemStatus::Pending,
199 error: None,
200 duration_ms: None,
201 size_bucket: bucket,
202 trace_ctx: Some(trace_ctx.clone()),
203 attempt_id: Some(stack_ids::AttemptId::generate()),
204 trial_id: None,
205 })
206 .collect();
207
208 BatchJob {
209 id: String::new(),
210 resource_key: resource_key.to_string(),
211 operation: operation.to_string(),
212 overwrite_policy,
213 items: batch_items,
214 status: BatchJobStatus::Queued,
215 created_at: String::new(),
216 started_at: None,
217 completed_at: None,
218 reordered: false,
219 reorder_note: None,
220 }
221}