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