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