1use mold_core::Config;
2use mold_inference::InferenceEngine;
3use std::path::PathBuf;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::{Arc, RwLock};
6use std::time::Instant;
7use tokio::sync::Mutex;
8
9use mold_inference::shared_pool::SharedPool;
10
11use crate::model_cache::ModelCache;
12
13#[derive(Debug, Clone, Default)]
14pub struct EngineSnapshot {
15 pub model_name: Option<String>,
17 pub is_loaded: bool,
18 pub cached_models: Vec<String>,
20}
21
22#[derive(Debug, Clone)]
23pub struct ActiveGenerationSnapshot {
24 pub model: String,
25 pub prompt_sha256: String,
26 pub started_at_unix_ms: u64,
27 pub started_at: Instant,
28}
29
30pub enum SseMessage {
34 Progress(mold_core::SseProgressEvent),
35 Complete(mold_core::SseCompleteEvent),
36 UpscaleComplete(mold_core::SseUpscaleCompleteEvent),
37 Error(mold_core::SseErrorEvent),
38}
39
40pub struct GenerationJob {
42 pub request: mold_core::GenerateRequest,
43 pub progress_tx: Option<tokio::sync::mpsc::UnboundedSender<SseMessage>>,
45 pub result_tx: tokio::sync::oneshot::Sender<Result<GenerationJobResult, String>>,
47 pub output_dir: Option<PathBuf>,
49}
50
51pub struct GenerationJobResult {
52 pub response: mold_core::GenerateResponse,
53 pub image: mold_core::ImageData,
54}
55
56#[derive(Clone)]
58pub struct QueueHandle {
59 job_tx: tokio::sync::mpsc::Sender<GenerationJob>,
60 pending_count: Arc<AtomicUsize>,
61}
62
63impl QueueHandle {
64 pub fn new(job_tx: tokio::sync::mpsc::Sender<GenerationJob>) -> Self {
65 Self {
66 job_tx,
67 pending_count: Arc::new(AtomicUsize::new(0)),
68 }
69 }
70
71 pub async fn submit(&self, job: GenerationJob) -> Result<usize, String> {
73 let position = self.pending_count.fetch_add(1, Ordering::SeqCst);
74 if let Err(_e) = self.job_tx.send(job).await {
75 self.pending_count.fetch_sub(1, Ordering::SeqCst);
76 return Err("generation queue shut down".to_string());
77 }
78 #[cfg(feature = "metrics")]
79 {
80 crate::metrics::record_queue_submit();
81 crate::metrics::record_queue_depth(self.pending_count.load(Ordering::SeqCst));
82 }
83 Ok(position)
84 }
85
86 pub fn decrement(&self) {
87 self.pending_count.fetch_sub(1, Ordering::SeqCst);
88 }
89
90 pub fn pending(&self) -> usize {
91 self.pending_count.load(Ordering::SeqCst)
92 }
93}
94
95#[derive(Clone)]
98pub struct AppState {
99 pub model_cache: Arc<Mutex<ModelCache>>,
100 pub engine_snapshot: Arc<tokio::sync::RwLock<EngineSnapshot>>,
101 pub active_generation: Arc<RwLock<Option<ActiveGenerationSnapshot>>>,
105 pub config: Arc<tokio::sync::RwLock<Config>>,
106 pub start_time: Instant,
107 pub model_load_lock: Arc<Mutex<()>>,
109 pub pull_lock: Arc<Mutex<()>>,
111 pub queue: QueueHandle,
113 pub shared_pool: Arc<std::sync::Mutex<SharedPool>>,
115 pub shutdown_tx: Arc<tokio::sync::Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
117 pub upscaler_cache: Arc<std::sync::Mutex<Option<Box<dyn mold_inference::UpscaleEngine>>>>,
119}
120
121const DEFAULT_MAX_CACHED_MODELS: usize = 3;
123
124impl AppState {
125 pub fn new(engine: Box<dyn InferenceEngine>, config: Config, queue: QueueHandle) -> Self {
127 let name = engine.model_name().to_string();
128 let loaded = engine.is_loaded();
129 let mut cache = ModelCache::new(DEFAULT_MAX_CACHED_MODELS);
130 cache.insert(engine, 0);
131 let snapshot = EngineSnapshot {
132 model_name: Some(name),
133 is_loaded: loaded,
134 cached_models: cache.cached_model_names(),
135 };
136 Self {
137 model_cache: Arc::new(Mutex::new(cache)),
138 engine_snapshot: Arc::new(tokio::sync::RwLock::new(snapshot)),
139 active_generation: Arc::new(RwLock::new(None)),
140 config: Arc::new(tokio::sync::RwLock::new(config)),
141 start_time: Instant::now(),
142 model_load_lock: Arc::new(Mutex::new(())),
143 pull_lock: Arc::new(Mutex::new(())),
144 queue,
145 shared_pool: Arc::new(std::sync::Mutex::new(SharedPool::new())),
146 shutdown_tx: Arc::new(tokio::sync::Mutex::new(None)),
147 upscaler_cache: Arc::new(std::sync::Mutex::new(None)),
148 }
149 }
150
151 pub fn empty(config: Config, queue: QueueHandle) -> Self {
153 Self {
154 model_cache: Arc::new(Mutex::new(ModelCache::new(DEFAULT_MAX_CACHED_MODELS))),
155 engine_snapshot: Arc::new(tokio::sync::RwLock::new(EngineSnapshot::default())),
156 active_generation: Arc::new(RwLock::new(None)),
157 config: Arc::new(tokio::sync::RwLock::new(config)),
158 start_time: Instant::now(),
159 model_load_lock: Arc::new(Mutex::new(())),
160 pull_lock: Arc::new(Mutex::new(())),
161 queue,
162 shared_pool: Arc::new(std::sync::Mutex::new(SharedPool::new())),
163 shutdown_tx: Arc::new(tokio::sync::Mutex::new(None)),
164 upscaler_cache: Arc::new(std::sync::Mutex::new(None)),
165 }
166 }
167
168 #[cfg(test)]
169 pub fn with_engine(engine: impl InferenceEngine + 'static) -> Self {
170 let (tx, _rx) = tokio::sync::mpsc::channel(16);
171 let queue = QueueHandle::new(tx);
172 let name = engine.model_name().to_string();
173 let loaded = engine.is_loaded();
174 let mut cache = ModelCache::new(DEFAULT_MAX_CACHED_MODELS);
175 cache.insert(Box::new(engine), 0);
176 let snapshot = EngineSnapshot {
177 model_name: Some(name),
178 is_loaded: loaded,
179 cached_models: cache.cached_model_names(),
180 };
181 Self {
182 model_cache: Arc::new(Mutex::new(cache)),
183 engine_snapshot: Arc::new(tokio::sync::RwLock::new(snapshot)),
184 active_generation: Arc::new(RwLock::new(None)),
185 config: Arc::new(tokio::sync::RwLock::new(Config::default())),
186 start_time: Instant::now(),
187 model_load_lock: Arc::new(Mutex::new(())),
188 pull_lock: Arc::new(Mutex::new(())),
189 queue,
190 shared_pool: Arc::new(std::sync::Mutex::new(SharedPool::new())),
191 shutdown_tx: Arc::new(tokio::sync::Mutex::new(None)),
192 upscaler_cache: Arc::new(std::sync::Mutex::new(None)),
193 }
194 }
195
196 #[cfg(test)]
198 pub fn with_engine_and_queue(
199 engine: impl InferenceEngine + 'static,
200 ) -> (Self, tokio::sync::mpsc::Receiver<GenerationJob>) {
201 let (tx, rx) = tokio::sync::mpsc::channel(16);
202 let queue = QueueHandle::new(tx);
203 let name = engine.model_name().to_string();
204 let loaded = engine.is_loaded();
205 let mut cache = ModelCache::new(DEFAULT_MAX_CACHED_MODELS);
206 cache.insert(Box::new(engine), 0);
207 let snapshot = EngineSnapshot {
208 model_name: Some(name),
209 is_loaded: loaded,
210 cached_models: cache.cached_model_names(),
211 };
212 let state = Self {
213 model_cache: Arc::new(Mutex::new(cache)),
214 engine_snapshot: Arc::new(tokio::sync::RwLock::new(snapshot)),
215 active_generation: Arc::new(RwLock::new(None)),
216 config: Arc::new(tokio::sync::RwLock::new(Config::default())),
217 start_time: Instant::now(),
218 model_load_lock: Arc::new(Mutex::new(())),
219 pull_lock: Arc::new(Mutex::new(())),
220 queue,
221 shared_pool: Arc::new(std::sync::Mutex::new(SharedPool::new())),
222 shutdown_tx: Arc::new(tokio::sync::Mutex::new(None)),
223 upscaler_cache: Arc::new(std::sync::Mutex::new(None)),
224 };
225 (state, rx)
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn engine_snapshot_default_is_unloaded() {
235 let snap = EngineSnapshot::default();
236 assert!(snap.model_name.is_none());
237 assert!(!snap.is_loaded);
238 }
239
240 #[test]
241 fn active_generation_snapshot_stores_fields() {
242 let snap = ActiveGenerationSnapshot {
243 model: "flux-dev:q8".to_string(),
244 prompt_sha256: "abc123".to_string(),
245 started_at_unix_ms: 1700000000000,
246 started_at: std::time::Instant::now(),
247 };
248 assert_eq!(snap.model, "flux-dev:q8");
249 assert_eq!(snap.prompt_sha256, "abc123");
250 assert_eq!(snap.started_at_unix_ms, 1700000000000);
251 }
252
253 #[test]
254 fn queue_handle_pending_starts_at_zero() {
255 let (tx, _rx) = tokio::sync::mpsc::channel::<GenerationJob>(16);
256 let handle = QueueHandle::new(tx);
257 assert_eq!(handle.pending(), 0);
258 }
259
260 #[test]
261 fn upscaler_cache_starts_empty() {
262 let config = mold_core::Config::default();
263 let state = AppState::empty(config, QueueHandle::new(tokio::sync::mpsc::channel(1).0));
264 let cache = state.upscaler_cache.lock().unwrap();
265 assert!(cache.is_none());
266 }
267
268 #[test]
269 fn upscaler_cache_cleared_by_setting_none() {
270 let config = mold_core::Config::default();
271 let state = AppState::empty(config, QueueHandle::new(tokio::sync::mpsc::channel(1).0));
272 {
273 let mut cache = state.upscaler_cache.lock().unwrap();
274 *cache = None;
275 }
276 let cache = state.upscaler_cache.lock().unwrap();
277 assert!(cache.is_none());
278 }
279}