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 crate::model_cache::ModelCache;
10
11#[derive(Debug, Clone, Default)]
12pub struct EngineSnapshot {
13 pub model_name: Option<String>,
15 pub is_loaded: bool,
16 pub cached_models: Vec<String>,
18}
19
20#[derive(Debug, Clone)]
21pub struct ActiveGenerationSnapshot {
22 pub model: String,
23 pub prompt_sha256: String,
24 pub started_at_unix_ms: u64,
25 pub started_at: Instant,
26}
27
28pub enum SseMessage {
32 Progress(mold_core::SseProgressEvent),
33 Complete(mold_core::SseCompleteEvent),
34 Error(mold_core::SseErrorEvent),
35}
36
37pub struct GenerationJob {
39 pub request: mold_core::GenerateRequest,
40 pub progress_tx: Option<tokio::sync::mpsc::UnboundedSender<SseMessage>>,
42 pub result_tx: tokio::sync::oneshot::Sender<Result<GenerationJobResult, String>>,
44 pub output_dir: Option<PathBuf>,
46}
47
48pub struct GenerationJobResult {
49 pub response: mold_core::GenerateResponse,
50 pub image: mold_core::ImageData,
51}
52
53#[derive(Clone)]
55pub struct QueueHandle {
56 job_tx: tokio::sync::mpsc::Sender<GenerationJob>,
57 pending_count: Arc<AtomicUsize>,
58}
59
60impl QueueHandle {
61 pub fn new(job_tx: tokio::sync::mpsc::Sender<GenerationJob>) -> Self {
62 Self {
63 job_tx,
64 pending_count: Arc::new(AtomicUsize::new(0)),
65 }
66 }
67
68 pub async fn submit(&self, job: GenerationJob) -> Result<usize, String> {
70 let position = self.pending_count.fetch_add(1, Ordering::SeqCst);
71 if let Err(_e) = self.job_tx.send(job).await {
72 self.pending_count.fetch_sub(1, Ordering::SeqCst);
73 return Err("generation queue shut down".to_string());
74 }
75 Ok(position)
76 }
77
78 pub fn decrement(&self) {
79 self.pending_count.fetch_sub(1, Ordering::SeqCst);
80 }
81
82 pub fn pending(&self) -> usize {
83 self.pending_count.load(Ordering::SeqCst)
84 }
85}
86
87#[derive(Clone)]
90pub struct AppState {
91 pub model_cache: Arc<Mutex<ModelCache>>,
92 pub engine_snapshot: Arc<tokio::sync::RwLock<EngineSnapshot>>,
93 pub active_generation: Arc<RwLock<Option<ActiveGenerationSnapshot>>>,
97 pub config: Arc<tokio::sync::RwLock<Config>>,
98 pub start_time: Instant,
99 pub model_load_lock: Arc<Mutex<()>>,
101 pub pull_lock: Arc<Mutex<()>>,
103 pub queue: QueueHandle,
105}
106
107const DEFAULT_MAX_CACHED_MODELS: usize = 3;
109
110impl AppState {
111 pub fn new(engine: Box<dyn InferenceEngine>, config: Config, queue: QueueHandle) -> Self {
113 let name = engine.model_name().to_string();
114 let loaded = engine.is_loaded();
115 let mut cache = ModelCache::new(DEFAULT_MAX_CACHED_MODELS);
116 cache.insert(engine, 0);
117 let snapshot = EngineSnapshot {
118 model_name: Some(name),
119 is_loaded: loaded,
120 cached_models: cache.cached_model_names(),
121 };
122 Self {
123 model_cache: Arc::new(Mutex::new(cache)),
124 engine_snapshot: Arc::new(tokio::sync::RwLock::new(snapshot)),
125 active_generation: Arc::new(RwLock::new(None)),
126 config: Arc::new(tokio::sync::RwLock::new(config)),
127 start_time: Instant::now(),
128 model_load_lock: Arc::new(Mutex::new(())),
129 pull_lock: Arc::new(Mutex::new(())),
130 queue,
131 }
132 }
133
134 pub fn empty(config: Config, queue: QueueHandle) -> Self {
136 Self {
137 model_cache: Arc::new(Mutex::new(ModelCache::new(DEFAULT_MAX_CACHED_MODELS))),
138 engine_snapshot: Arc::new(tokio::sync::RwLock::new(EngineSnapshot::default())),
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 }
146 }
147
148 #[cfg(test)]
149 pub fn with_engine(engine: impl InferenceEngine + 'static) -> Self {
150 let (tx, _rx) = tokio::sync::mpsc::channel(16);
151 let queue = QueueHandle::new(tx);
152 let name = engine.model_name().to_string();
153 let loaded = engine.is_loaded();
154 let mut cache = ModelCache::new(DEFAULT_MAX_CACHED_MODELS);
155 cache.insert(Box::new(engine), 0);
156 let snapshot = EngineSnapshot {
157 model_name: Some(name),
158 is_loaded: loaded,
159 cached_models: cache.cached_model_names(),
160 };
161 Self {
162 model_cache: Arc::new(Mutex::new(cache)),
163 engine_snapshot: Arc::new(tokio::sync::RwLock::new(snapshot)),
164 active_generation: Arc::new(RwLock::new(None)),
165 config: Arc::new(tokio::sync::RwLock::new(Config::default())),
166 start_time: Instant::now(),
167 model_load_lock: Arc::new(Mutex::new(())),
168 pull_lock: Arc::new(Mutex::new(())),
169 queue,
170 }
171 }
172
173 #[cfg(test)]
175 pub fn with_engine_and_queue(
176 engine: impl InferenceEngine + 'static,
177 ) -> (Self, tokio::sync::mpsc::Receiver<GenerationJob>) {
178 let (tx, rx) = tokio::sync::mpsc::channel(16);
179 let queue = QueueHandle::new(tx);
180 let name = engine.model_name().to_string();
181 let loaded = engine.is_loaded();
182 let mut cache = ModelCache::new(DEFAULT_MAX_CACHED_MODELS);
183 cache.insert(Box::new(engine), 0);
184 let snapshot = EngineSnapshot {
185 model_name: Some(name),
186 is_loaded: loaded,
187 cached_models: cache.cached_model_names(),
188 };
189 let state = Self {
190 model_cache: Arc::new(Mutex::new(cache)),
191 engine_snapshot: Arc::new(tokio::sync::RwLock::new(snapshot)),
192 active_generation: Arc::new(RwLock::new(None)),
193 config: Arc::new(tokio::sync::RwLock::new(Config::default())),
194 start_time: Instant::now(),
195 model_load_lock: Arc::new(Mutex::new(())),
196 pull_lock: Arc::new(Mutex::new(())),
197 queue,
198 };
199 (state, rx)
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn engine_snapshot_default_is_unloaded() {
209 let snap = EngineSnapshot::default();
210 assert!(snap.model_name.is_none());
211 assert!(!snap.is_loaded);
212 }
213
214 #[test]
215 fn active_generation_snapshot_stores_fields() {
216 let snap = ActiveGenerationSnapshot {
217 model: "flux-dev:q8".to_string(),
218 prompt_sha256: "abc123".to_string(),
219 started_at_unix_ms: 1700000000000,
220 started_at: std::time::Instant::now(),
221 };
222 assert_eq!(snap.model, "flux-dev:q8");
223 assert_eq!(snap.prompt_sha256, "abc123");
224 assert_eq!(snap.started_at_unix_ms, 1700000000000);
225 }
226
227 #[test]
228 fn queue_handle_pending_starts_at_zero() {
229 let (tx, _rx) = tokio::sync::mpsc::channel::<GenerationJob>(16);
230 let handle = QueueHandle::new(tx);
231 assert_eq!(handle.pending(), 0);
232 }
233}