Skip to main content

mold_server/
queue.rs

1use std::sync::Arc;
2
3use base64::Engine as _;
4use mold_core::{SseCompleteEvent, SseErrorEvent, SseProgressEvent};
5use sha2::{Digest, Sha256};
6use std::time::{Instant, SystemTime, UNIX_EPOCH};
7
8use crate::model_manager;
9use crate::state::{
10    ActiveGenerationSnapshot, AppState, GenerationJob, GenerationJobResult, SseMessage,
11};
12
13/// Convert an inference-crate progress event to an SSE wire event.
14fn progress_to_sse(event: mold_inference::ProgressEvent) -> SseProgressEvent {
15    event.into()
16}
17
18/// Strips backtrace frames from candle error messages.
19pub(crate) fn clean_error_message(e: &anyhow::Error) -> String {
20    let full = format!("{e}");
21    let mut lines: Vec<&str> = Vec::new();
22    for line in full.lines() {
23        let trimmed = line.trim_start();
24        if (trimmed.starts_with("0:") || trimmed.starts_with("1:"))
25            && trimmed.len() > 3
26            && trimmed
27                .as_bytes()
28                .first()
29                .is_some_and(|b| b.is_ascii_digit())
30        {
31            break;
32        }
33        if trimmed.len() > 2
34            && trimmed.as_bytes()[0].is_ascii_digit()
35            && trimmed.contains("::")
36            && trimmed.contains("at ")
37        {
38            break;
39        }
40        lines.push(line);
41    }
42    let msg = lines.join("\n").trim().to_string();
43    if msg.is_empty() {
44        format!("{}", e.root_cause())
45    } else {
46        msg
47    }
48}
49
50fn set_active_generation(state: &AppState, model: &str, prompt: &str) {
51    let prompt_sha256 = format!("{:x}", Sha256::digest(prompt.as_bytes()));
52    let started_at_unix_ms = SystemTime::now()
53        .duration_since(UNIX_EPOCH)
54        .unwrap_or_default()
55        .as_millis() as u64;
56
57    let mut active = state
58        .active_generation
59        .write()
60        .unwrap_or_else(|e| e.into_inner());
61    *active = Some(ActiveGenerationSnapshot {
62        model: model.to_string(),
63        prompt_sha256,
64        started_at_unix_ms,
65        started_at: Instant::now(),
66    });
67}
68
69fn clear_active_generation(state: &AppState) {
70    let mut active = state
71        .active_generation
72        .write()
73        .unwrap_or_else(|e| e.into_inner());
74    *active = None;
75}
76
77fn save_image_to_dir(
78    dir: &std::path::Path,
79    img: &mold_core::ImageData,
80    model: &str,
81    batch_size: u32,
82) {
83    if let Err(e) = std::fs::create_dir_all(dir) {
84        tracing::warn!("failed to create output dir {}: {e}", dir.display());
85        return;
86    }
87    let timestamp_ms = SystemTime::now()
88        .duration_since(UNIX_EPOCH)
89        .unwrap_or_default()
90        .as_millis() as u64;
91    let ext = img.format.to_string();
92    let filename =
93        mold_core::default_output_filename(model, timestamp_ms, &ext, batch_size, img.index);
94    let path = dir.join(&filename);
95    match std::fs::write(&path, &img.data) {
96        Ok(()) => tracing::info!("saved image to {}", path.display()),
97        Err(e) => tracing::warn!("failed to save image to {}: {e}", path.display()),
98    }
99}
100
101/// Runs the generation queue worker loop. Processes one job at a time (FIFO).
102/// Exits when the sender half of the channel is dropped (server shutdown).
103pub async fn run_queue_worker(
104    mut job_rx: tokio::sync::mpsc::Receiver<GenerationJob>,
105    state: AppState,
106) {
107    tracing::debug!("generation queue worker started");
108    while let Some(job) = job_rx.recv().await {
109        process_job(&state, job).await;
110        state.queue.decrement();
111    }
112    tracing::info!("generation queue worker shutting down");
113}
114
115async fn process_job(state: &AppState, job: GenerationJob) {
116    // Check if client already disconnected before doing any work
117    if job.result_tx.is_closed() {
118        tracing::debug!("skipping queued job — client disconnected");
119        return;
120    }
121
122    // Send "now processing" event (position 0)
123    if let Some(ref tx) = job.progress_tx {
124        let _ = tx.send(SseMessage::Progress(SseProgressEvent::Queued {
125            position: 0,
126        }));
127    }
128
129    // 1. Ensure model is ready (with progress forwarding)
130    let progress_callback = job.progress_tx.as_ref().map(|tx| {
131        let tx = tx.clone();
132        Arc::new(move |event: mold_inference::ProgressEvent| {
133            let _ = tx.send(SseMessage::Progress(progress_to_sse(event)));
134        }) as model_manager::EngineProgressCallback
135    });
136
137    if let Err(api_err) =
138        model_manager::ensure_model_ready(state, &job.request.model, progress_callback).await
139    {
140        let err_msg = api_err.error.clone();
141        if let Some(ref tx) = job.progress_tx {
142            let _ = tx.send(SseMessage::Error(SseErrorEvent {
143                message: err_msg.clone(),
144            }));
145        }
146        let _ = job.result_tx.send(Err(err_msg));
147        return;
148    }
149
150    // 2. Low-memory warning (MPS/unified memory only — observability aid)
151    #[cfg(target_os = "macos")]
152    if let Some(available) = mold_inference::device::available_system_memory_bytes() {
153        if available < 1_000_000_000 {
154            tracing::warn!(
155                available_mb = available / 1_000_000,
156                "low memory before inference — system may become unstable"
157            );
158        }
159    }
160
161    // 3. Run inference in spawn_blocking
162    let model_cache = state.model_cache.clone();
163    let active_gen = state.active_generation.clone();
164    let gen_state = state.clone();
165    let gen_req = job.request.clone();
166    let progress_tx = job.progress_tx.clone();
167
168    let result = tokio::task::spawn_blocking(move || {
169        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
170            let mut guard = model_cache.blocking_lock();
171            let entry = guard.get_mut(&gen_req.model).ok_or_else(|| {
172                anyhow::anyhow!("no engine available after model readiness check")
173            })?;
174            let e = &mut entry.engine;
175            set_active_generation(&gen_state, &gen_req.model, &gen_req.prompt);
176
177            // Install progress callback for the generate phase
178            if let Some(ref ptx) = progress_tx {
179                let ptx = ptx.clone();
180                e.set_on_progress(Box::new(move |event| {
181                    let _ = ptx.send(SseMessage::Progress(progress_to_sse(event)));
182                }));
183            } else {
184                e.clear_on_progress();
185            }
186
187            let generate_result = e.generate(&gen_req);
188            if progress_tx.is_some() {
189                e.clear_on_progress();
190            }
191            clear_active_generation(&gen_state);
192            generate_result
193        }))
194    })
195    .await;
196
197    match result {
198        Ok(Ok(Ok(mut response))) => {
199            if response.images.is_empty() {
200                let err_msg = "generation error: engine returned no images".to_string();
201                if let Some(ref tx) = job.progress_tx {
202                    let _ = tx.send(SseMessage::Error(SseErrorEvent {
203                        message: err_msg.clone(),
204                    }));
205                }
206                let _ = job.result_tx.send(Err(err_msg));
207                return;
208            }
209            let img = response.images.remove(0);
210
211            // Save to output directory if configured
212            if let Some(ref dir) = job.output_dir {
213                let dir = dir.clone();
214                let img_clone = img.clone();
215                let model = job.request.model.clone();
216                let batch_size = job.request.batch_size;
217                tokio::task::spawn_blocking(move || {
218                    save_image_to_dir(&dir, &img_clone, &model, batch_size);
219                });
220            }
221
222            // Send SSE complete event
223            if let Some(ref tx) = job.progress_tx {
224                let _ = tx.send(SseMessage::Complete(SseCompleteEvent {
225                    image: base64::engine::general_purpose::STANDARD.encode(&img.data),
226                    format: img.format,
227                    width: img.width,
228                    height: img.height,
229                    seed_used: response.seed_used,
230                    generation_time_ms: response.generation_time_ms,
231                }));
232            }
233
234            // Send result through oneshot
235            let _ = job.result_tx.send(Ok(GenerationJobResult {
236                image: img,
237                response,
238            }));
239        }
240        Ok(Ok(Err(e))) => {
241            *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
242            tracing::error!("generation error: {e:#}");
243            let err_msg = format!("generation error: {}", clean_error_message(&e));
244            if let Some(ref tx) = job.progress_tx {
245                let _ = tx.send(SseMessage::Error(SseErrorEvent {
246                    message: err_msg.clone(),
247                }));
248            }
249            let _ = job.result_tx.send(Err(err_msg));
250        }
251        Ok(Err(panic_payload)) => {
252            *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
253            let msg = panic_payload
254                .downcast_ref::<String>()
255                .map(|s| s.as_str())
256                .or_else(|| panic_payload.downcast_ref::<&str>().copied())
257                .unwrap_or("unknown panic");
258            tracing::error!("inference panicked: {msg}");
259            let err_msg = format!("inference panicked: {msg}");
260            if let Some(ref tx) = job.progress_tx {
261                let _ = tx.send(SseMessage::Error(SseErrorEvent {
262                    message: err_msg.clone(),
263                }));
264            }
265            let _ = job.result_tx.send(Err(err_msg));
266        }
267        Err(join_err) => {
268            *active_gen.write().unwrap_or_else(|e| e.into_inner()) = None;
269            tracing::error!("inference task join error: {join_err:?}");
270            let err_msg = "inference task failed".to_string();
271            if let Some(ref tx) = job.progress_tx {
272                let _ = tx.send(SseMessage::Error(SseErrorEvent {
273                    message: err_msg.clone(),
274                }));
275            }
276            let _ = job.result_tx.send(Err(err_msg));
277        }
278    }
279}