Skip to main content

mold_server/
state.rs

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    /// Currently GPU-loaded model (None if no model on GPU).
14    pub model_name: Option<String>,
15    pub is_loaded: bool,
16    /// All models in the cache (loaded + unloaded), for status display.
17    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
28// ── Generation queue types ──────────────────────────────────────────────────
29
30/// Internal SSE message type used by both the queue worker and SSE streams.
31pub enum SseMessage {
32    Progress(mold_core::SseProgressEvent),
33    Complete(mold_core::SseCompleteEvent),
34    Error(mold_core::SseErrorEvent),
35}
36
37/// A generation job submitted to the queue worker.
38pub struct GenerationJob {
39    pub request: mold_core::GenerateRequest,
40    /// Channel to send SSE progress/complete/error events (None for non-streaming).
41    pub progress_tx: Option<tokio::sync::mpsc::UnboundedSender<SseMessage>>,
42    /// Oneshot to return the final result for non-streaming callers.
43    pub result_tx: tokio::sync::oneshot::Sender<Result<GenerationJobResult, String>>,
44    /// Pre-resolved output directory for server-side image saving.
45    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/// Handle for submitting jobs to the generation queue.
54#[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    /// Submit a generation job. Returns the queue position (0-based).
69    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// ── AppState ────────────────────────────────────────────────────────────────
88
89#[derive(Clone)]
90pub struct AppState {
91    pub model_cache: Arc<Mutex<ModelCache>>,
92    pub engine_snapshot: Arc<tokio::sync::RwLock<EngineSnapshot>>,
93    /// Uses std::sync::RwLock (not tokio) because it's only accessed from
94    /// synchronous contexts (inside spawn_blocking closures and brief reads).
95    /// Must never be held across an .await point.
96    pub active_generation: Arc<RwLock<Option<ActiveGenerationSnapshot>>>,
97    pub config: Arc<tokio::sync::RwLock<Config>>,
98    pub start_time: Instant,
99    /// Guards concurrent model loads and hot-swaps.
100    pub model_load_lock: Arc<Mutex<()>>,
101    /// Guards concurrent pulls — only one download at a time.
102    pub pull_lock: Arc<Mutex<()>>,
103    /// Generation request queue.
104    pub queue: QueueHandle,
105}
106
107/// Default maximum number of cached models (loaded + unloaded engine structs).
108const DEFAULT_MAX_CACHED_MODELS: usize = 3;
109
110impl AppState {
111    /// Create state with a pre-loaded engine (server starts with a configured model).
112    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    /// Create state with no engine (zero-config startup, models pulled on demand).
135    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    /// Create state with a queue whose receiver is returned for testing.
174    #[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}