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    /// Shutdown trigger for graceful shutdown via `/api/shutdown` endpoint.
110    pub shutdown_tx: Arc<tokio::sync::Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
111}
112
113/// Default maximum number of cached models (loaded + unloaded engine structs).
114const DEFAULT_MAX_CACHED_MODELS: usize = 3;
115
116impl AppState {
117    /// Create state with a pre-loaded engine (server starts with a configured model).
118    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    /// Create state with no engine (zero-config startup, models pulled on demand).
143    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    /// Create state with a queue whose receiver is returned for testing.
186    #[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}