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 pub shutdown_tx: Arc<tokio::sync::Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
111}
112
113const DEFAULT_MAX_CACHED_MODELS: usize = 3;
115
116impl AppState {
117 pub fn new(engine: Box<dyn InferenceEngine>, config: Config, queue: QueueHandle) -> Self {
119 let name = engine.model_name().to_string();
120 let loaded = engine.is_loaded();
121 let mut cache = ModelCache::new(DEFAULT_MAX_CACHED_MODELS);
122 cache.insert(engine, 0);
123 let snapshot = EngineSnapshot {
124 model_name: Some(name),
125 is_loaded: loaded,
126 cached_models: cache.cached_model_names(),
127 };
128 Self {
129 model_cache: Arc::new(Mutex::new(cache)),
130 engine_snapshot: Arc::new(tokio::sync::RwLock::new(snapshot)),
131 active_generation: Arc::new(RwLock::new(None)),
132 config: Arc::new(tokio::sync::RwLock::new(config)),
133 start_time: Instant::now(),
134 model_load_lock: Arc::new(Mutex::new(())),
135 pull_lock: Arc::new(Mutex::new(())),
136 queue,
137 shared_pool: Arc::new(std::sync::Mutex::new(SharedPool::new())),
138 shutdown_tx: Arc::new(tokio::sync::Mutex::new(None)),
139 }
140 }
141
142 pub fn empty(config: Config, queue: QueueHandle) -> Self {
144 Self {
145 model_cache: Arc::new(Mutex::new(ModelCache::new(DEFAULT_MAX_CACHED_MODELS))),
146 engine_snapshot: Arc::new(tokio::sync::RwLock::new(EngineSnapshot::default())),
147 active_generation: Arc::new(RwLock::new(None)),
148 config: Arc::new(tokio::sync::RwLock::new(config)),
149 start_time: Instant::now(),
150 model_load_lock: Arc::new(Mutex::new(())),
151 pull_lock: Arc::new(Mutex::new(())),
152 queue,
153 shared_pool: Arc::new(std::sync::Mutex::new(SharedPool::new())),
154 shutdown_tx: Arc::new(tokio::sync::Mutex::new(None)),
155 }
156 }
157
158 #[cfg(test)]
159 pub fn with_engine(engine: impl InferenceEngine + 'static) -> Self {
160 let (tx, _rx) = tokio::sync::mpsc::channel(16);
161 let queue = QueueHandle::new(tx);
162 let name = engine.model_name().to_string();
163 let loaded = engine.is_loaded();
164 let mut cache = ModelCache::new(DEFAULT_MAX_CACHED_MODELS);
165 cache.insert(Box::new(engine), 0);
166 let snapshot = EngineSnapshot {
167 model_name: Some(name),
168 is_loaded: loaded,
169 cached_models: cache.cached_model_names(),
170 };
171 Self {
172 model_cache: Arc::new(Mutex::new(cache)),
173 engine_snapshot: Arc::new(tokio::sync::RwLock::new(snapshot)),
174 active_generation: Arc::new(RwLock::new(None)),
175 config: Arc::new(tokio::sync::RwLock::new(Config::default())),
176 start_time: Instant::now(),
177 model_load_lock: Arc::new(Mutex::new(())),
178 pull_lock: Arc::new(Mutex::new(())),
179 queue,
180 shared_pool: Arc::new(std::sync::Mutex::new(SharedPool::new())),
181 shutdown_tx: Arc::new(tokio::sync::Mutex::new(None)),
182 }
183 }
184
185 #[cfg(test)]
187 pub fn with_engine_and_queue(
188 engine: impl InferenceEngine + 'static,
189 ) -> (Self, tokio::sync::mpsc::Receiver<GenerationJob>) {
190 let (tx, rx) = tokio::sync::mpsc::channel(16);
191 let queue = QueueHandle::new(tx);
192 let name = engine.model_name().to_string();
193 let loaded = engine.is_loaded();
194 let mut cache = ModelCache::new(DEFAULT_MAX_CACHED_MODELS);
195 cache.insert(Box::new(engine), 0);
196 let snapshot = EngineSnapshot {
197 model_name: Some(name),
198 is_loaded: loaded,
199 cached_models: cache.cached_model_names(),
200 };
201 let state = Self {
202 model_cache: Arc::new(Mutex::new(cache)),
203 engine_snapshot: Arc::new(tokio::sync::RwLock::new(snapshot)),
204 active_generation: Arc::new(RwLock::new(None)),
205 config: Arc::new(tokio::sync::RwLock::new(Config::default())),
206 start_time: Instant::now(),
207 model_load_lock: Arc::new(Mutex::new(())),
208 pull_lock: Arc::new(Mutex::new(())),
209 queue,
210 shared_pool: Arc::new(std::sync::Mutex::new(SharedPool::new())),
211 shutdown_tx: Arc::new(tokio::sync::Mutex::new(None)),
212 };
213 (state, rx)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn engine_snapshot_default_is_unloaded() {
223 let snap = EngineSnapshot::default();
224 assert!(snap.model_name.is_none());
225 assert!(!snap.is_loaded);
226 }
227
228 #[test]
229 fn active_generation_snapshot_stores_fields() {
230 let snap = ActiveGenerationSnapshot {
231 model: "flux-dev:q8".to_string(),
232 prompt_sha256: "abc123".to_string(),
233 started_at_unix_ms: 1700000000000,
234 started_at: std::time::Instant::now(),
235 };
236 assert_eq!(snap.model, "flux-dev:q8");
237 assert_eq!(snap.prompt_sha256, "abc123");
238 assert_eq!(snap.started_at_unix_ms, 1700000000000);
239 }
240
241 #[test]
242 fn queue_handle_pending_starts_at_zero() {
243 let (tx, _rx) = tokio::sync::mpsc::channel::<GenerationJob>(16);
244 let handle = QueueHandle::new(tx);
245 assert_eq!(handle.pending(), 0);
246 }
247}