1use crate::config::Config;
6use crate::core::audit::AuditLogger;
7use crate::core::budget::{BudgetManager, UnifiedBudgetLimits};
8use crate::core::cache::LLMCache;
9use crate::core::guardrails::GuardrailEngine;
10use crate::core::ip_access::IpAccessControl;
11use crate::core::keys::{DatabaseKeyRepository, KeyManager};
12use crate::core::observability::RuntimeObservability;
13use crate::core::pricing_service::PricingService;
14use crate::core::router::UnifiedRouter;
15use crate::core::teams::TeamManager;
16use crate::core::virtual_keys::RuntimeVirtualKeyManager;
17use crate::server::routes::ai::budgeted::BudgetedExecutor;
18use crate::storage::database::SeaOrmTeamRepository;
19use crate::utils::error::gateway_error::{GatewayError, Result};
20use crate::utils::sync::AtomicValue;
21use std::sync::Arc;
22use tokio::sync::Mutex;
23
24#[cfg(test)]
25#[path = "runtime_reload_tests.rs"]
26mod runtime_reload_tests;
27
28#[cfg(test)]
29#[path = "runtime_reload_profile.rs"]
30mod runtime_reload_profile;
31
32pub use super::runtime::RuntimeRevision;
33use super::runtime::{build_response_cache, build_runtime_revision};
34
35#[derive(Clone)]
50pub struct AppState {
51 pub config: Arc<AtomicValue<Config>>,
53 pub auth: Arc<crate::auth::AuthSystem>,
55 pub storage: Arc<crate::storage::StorageLayer>,
57 pub pricing: Arc<PricingService>,
59 pub budget_limits: Arc<UnifiedBudgetLimits>,
61 pub budget_manager: Arc<BudgetManager>,
63 pub team_manager: Arc<TeamManager>,
65 pub key_manager: RuntimeVirtualKeyManager,
67 pub(crate) budgeted: BudgetedExecutor,
69 pub callbacks: RuntimeObservability,
71 pub audit_logger: Arc<AuditLogger>,
73 pub ip_access: Arc<IpAccessControl>,
75 #[cfg(feature = "mcp")]
76 pub(crate) mcp_inflight: Arc<super::routes::mcp::Inflight>,
77 pub(super) runtime: Arc<AtomicValue<RuntimeRevision>>,
78 #[cfg(feature = "a2a")]
79 pub(crate) a2a_tasks: Arc<crate::server::routes::a2a::TaskOwners>,
80 pub(super) apply_lock: Arc<Mutex<()>>,
81 pub(super) config_sync: Option<Arc<super::config_sync::ConfigSync>>,
82}
83
84impl AppState {
85 pub fn new_with_unified_router(
87 config: Config,
88 auth: crate::auth::AuthSystem,
89 unified_router: crate::core::router::UnifiedRouter,
90 storage: crate::storage::StorageLayer,
91 pricing: Arc<PricingService>,
92 budget_limits: Arc<UnifiedBudgetLimits>,
93 ) -> Self {
94 let redis = storage.redis.clone();
95 let config = Arc::new(config);
96 let revision = RuntimeRevision {
97 generation: 0,
98 unified_router: Arc::new(unified_router),
99 guardrails: Arc::new(GuardrailEngine::disabled()),
100 response_cache: build_response_cache(&config, redis),
101 config: Arc::clone(&config),
102 };
103 Self::new_with_runtime(revision, auth, storage, pricing, budget_limits)
104 }
105
106 pub fn new_with_runtime(
108 revision: RuntimeRevision,
109 auth: crate::auth::AuthSystem,
110 storage: crate::storage::StorageLayer,
111 pricing: Arc<PricingService>,
112 budget_limits: Arc<UnifiedBudgetLimits>,
113 ) -> Self {
114 let storage = Arc::new(storage);
115 let key_manager = KeyManager::new(DatabaseKeyRepository::new(storage.clone()))
116 .with_hmac_secret(revision.config.gateway.auth.api_key_hmac_secret.clone());
117 let budget_manager = Arc::new(BudgetManager::new());
118 let budgeted = BudgetedExecutor::new(
119 budget_limits.clone(),
120 budget_manager.clone(),
121 pricing.clone(),
122 key_manager.clone(),
123 );
124 let team_manager = Arc::new(TeamManager::new(Arc::new(SeaOrmTeamRepository::new(
125 storage.database.clone(),
126 ))));
127 Self {
128 config: Arc::new(AtomicValue::from(Arc::clone(&revision.config))),
129 auth: Arc::new(auth),
130 storage,
131 pricing,
132 budget_limits,
133 budget_manager,
134 team_manager,
135 key_manager,
136 budgeted,
137 callbacks: RuntimeObservability::disabled(),
138 audit_logger: Arc::new(AuditLogger::disabled()),
139 ip_access: Arc::new(IpAccessControl::disabled()),
140 #[cfg(feature = "mcp")]
141 mcp_inflight: Arc::new(super::routes::mcp::Inflight::default()),
142 runtime: Arc::new(AtomicValue::new(revision)),
143 #[cfg(feature = "a2a")]
144 a2a_tasks: Arc::default(),
145 apply_lock: Arc::new(Mutex::new(())),
146 config_sync: None,
147 }
148 }
149
150 pub fn with_callbacks(mut self, callbacks: RuntimeObservability) -> Self {
152 self.callbacks = callbacks;
153 self
154 }
155
156 pub fn with_audit_logger(mut self, audit_logger: Arc<AuditLogger>) -> Self {
158 self.audit_logger = audit_logger;
159 self
160 }
161
162 pub fn with_request_policies(
164 self,
165 guardrails: Arc<GuardrailEngine>,
166 ip_access: Arc<IpAccessControl>,
167 ) -> Self {
168 let current = self.runtime.load();
169 self.runtime.store(RuntimeRevision {
170 generation: current.generation,
171 config: Arc::clone(¤t.config),
172 unified_router: Arc::clone(¤t.unified_router),
173 guardrails,
174 response_cache: current.response_cache.clone(),
175 });
176 let mut this = self;
177 this.ip_access = ip_access;
178 this
179 }
180
181 pub fn pin_runtime(&self) -> Arc<RuntimeRevision> {
187 self.runtime.load()
188 }
189
190 pub fn unified_router(&self) -> Arc<UnifiedRouter> {
192 Arc::clone(&self.pin_runtime().unified_router)
193 }
194
195 pub fn guardrails(&self) -> Arc<GuardrailEngine> {
197 Arc::clone(&self.pin_runtime().guardrails)
198 }
199
200 pub fn response_cache(&self) -> Option<Arc<LLMCache>> {
202 self.pin_runtime().response_cache.clone()
203 }
204
205 pub async fn apply_runtime(&self, candidate: Config) -> Result<u64> {
212 let generation = self
213 .apply_runtime_at(candidate, self.pin_runtime().generation)
214 .await?;
215 self.notify_runtime_revision(generation).await?;
216 Ok(generation)
217 }
218
219 pub(crate) async fn apply_runtime_at(&self, candidate: Config, expected: u64) -> Result<u64> {
220 let _guard = self.apply_lock.lock().await;
221 if self.pin_runtime().generation != expected {
222 return Err(GatewayError::Conflict(
223 "Runtime revision changed; reload configuration before retrying".into(),
224 ));
225 }
226 let generation = expected
227 .checked_add(1)
228 .ok_or_else(|| GatewayError::Config("runtime generation overflow".into()))?;
229 let revision = build_runtime_revision(
230 candidate,
231 generation,
232 Arc::clone(&self.pricing),
233 Arc::clone(&self.storage.redis),
234 )
235 .await
236 .inspect_err(|_| {
237 if let Some(sync) = &self.config_sync {
238 sync.status.write().last_apply_error =
239 Some("Runtime candidate validation/build failed".into());
240 }
241 })?;
242 if let Some(sync) = &self.config_sync {
243 let ciphertext = sync.encrypt(&revision.config, generation)?;
244 self.storage
245 .database
246 .commit_runtime_config(expected, ciphertext)
247 .await
248 .inspect_err(|_| {
249 sync.status.write().last_sync_error =
250 Some("Authoritative configuration commit failed".into());
251 })?;
252 sync.status.write().observed_revision = generation;
253 }
254 self.publish_runtime(revision);
255 if let Some(sync) = &self.config_sync {
256 sync.status.write().last_apply_error = None;
257 }
258 Ok(generation)
259 }
260
261 pub(crate) async fn notify_runtime_revision(&self, generation: u64) -> Result<()> {
263 if let Some(sync) = &self.config_sync {
264 let published = tokio::time::timeout(
265 std::time::Duration::from_secs(5),
266 self.storage.redis.publish(
267 super::config_sync::REVISION_CHANNEL,
268 &generation.to_string(),
269 ),
270 )
271 .await;
272 if !matches!(published, Ok(Ok(()))) {
273 sync.status.write().last_sync_error =
274 Some("Revision committed but notification failed".into());
275 return Err(GatewayError::Storage(format!(
276 "Runtime revision {generation} committed and applied, but notification failed; peers reconcile from the database"
277 )));
278 }
279 sync.status.write().last_sync_error = None;
280 }
281 Ok(())
282 }
283
284 pub(super) fn publish_runtime(&self, revision: RuntimeRevision) {
285 let previous = self.runtime.load();
286 revision
287 .unified_router
288 .inherit_runtime_state(&previous.unified_router);
289 if let Some(cache) = previous.response_cache.as_ref() {
294 cache.shutdown();
295 }
296 let config = Arc::clone(&revision.config);
297 self.runtime.store(revision);
298 self.config.store_arc(config);
299 }
300
301 pub fn config(&self) -> Arc<Config> {
307 self.config.load()
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use crate::config::models::provider::ProviderConfig;
315 use crate::core::guardrails::PromptInjectionConfig;
316 use crate::server::HttpServer;
317
318 async fn test_app_state() -> AppState {
319 let mut config = crate::server::valid_test_config();
320 config.gateway.storage.database.enabled = false;
321 config.gateway.storage.redis.enabled = false;
322 config.gateway.pricing.source = None;
323 match HttpServer::new(&config).await {
324 Ok(server) => server.state().clone(),
325 Err(error) => panic!("test AppState startup failed: {error}"),
326 }
327 }
328
329 fn extra_provider(name: &str) -> ProviderConfig {
330 ProviderConfig {
331 name: name.to_string(),
332 provider_type: "openai".to_string(),
333 api_key: "sk-test".to_string(),
334 ..ProviderConfig::default()
335 }
336 }
337
338 #[tokio::test]
339 async fn apply_runtime_successful_swap_publishes_one_generation() {
340 let state = test_app_state().await;
341 let pinned = state.pin_runtime();
342 assert_eq!(pinned.generation, 0);
343 assert!(pinned.response_cache.is_none());
344
345 let mut next = (*state.config()).clone();
346 next.gateway.cache.enabled = true;
347 next.gateway.guardrails.enabled = false;
348 next.gateway.providers.push(extra_provider("test-openai-2"));
349
350 let generation = state
351 .apply_runtime(next)
352 .await
353 .expect("valid candidate should apply");
354 assert_eq!(generation, 1);
355
356 let live = state.pin_runtime();
357 assert_eq!(live.generation, 1);
358 assert!(live.response_cache.is_some());
359 assert!(!live.guardrails.is_enabled());
360 assert!(
361 live.unified_router.list_deployments().len()
362 > pinned.unified_router.list_deployments().len()
363 );
364 assert!(Arc::ptr_eq(&live.config, &state.config.load()));
365 assert!(!Arc::ptr_eq(&live.unified_router, &pinned.unified_router));
366 assert!(!Arc::ptr_eq(&live.guardrails, &pinned.guardrails));
367
368 assert_eq!(pinned.generation, 0);
369 assert!(pinned.response_cache.is_none());
370 assert!(pinned.guardrails.is_enabled());
371 }
372
373 #[tokio::test]
374 async fn apply_runtime_failed_build_leaves_previous_revision_active() {
375 let state = test_app_state().await;
376 let before = state.pin_runtime();
377 let router = Arc::clone(&before.unified_router);
378
379 let mut invalid = (*state.config()).clone();
380 let mut injection = invalid
381 .gateway
382 .guardrails
383 .prompt_injection
384 .clone()
385 .unwrap_or_else(PromptInjectionConfig::new);
386 injection.enabled = true;
387 injection.custom_patterns.push("(".to_string());
388 invalid.gateway.guardrails.prompt_injection = Some(injection);
389
390 let error = state
391 .apply_runtime(invalid)
392 .await
393 .expect_err("invalid guardrails must fail apply");
394 match error {
395 GatewayError::Config(message) => {
396 assert!(
397 message.to_lowercase().contains("guardrail")
398 || message.to_lowercase().contains("pattern"),
399 "unexpected apply error: {message}"
400 );
401 }
402 other => panic!("expected config error, got {other:?}"),
403 }
404
405 let after = state.pin_runtime();
406 assert_eq!(after.generation, before.generation);
407 assert!(Arc::ptr_eq(&after.unified_router, &router));
408 assert!(Arc::ptr_eq(&after.guardrails, &before.guardrails));
409 assert_eq!(after.generation, 0);
410 }
411
412 #[tokio::test]
413 async fn in_flight_pin_stays_on_old_generation_after_apply() {
414 let state = test_app_state().await;
415 let in_flight = Arc::clone(&state.pin_runtime());
416
417 let mut next = (*state.config()).clone();
418 next.gateway.cache.enabled = true;
419 next.gateway.guardrails.enabled = false;
420 next.gateway
421 .providers
422 .push(extra_provider("test-openai-inflight"));
423
424 state
425 .apply_runtime(next)
426 .await
427 .expect("valid candidate should apply");
428
429 let live = state.pin_runtime();
430 assert_eq!(live.generation, in_flight.generation + 1);
431 assert_eq!(in_flight.generation, 0);
432 assert!(!Arc::ptr_eq(
433 &in_flight.unified_router,
434 &live.unified_router
435 ));
436 assert!(!Arc::ptr_eq(&in_flight.guardrails, &live.guardrails));
437 assert!(in_flight.response_cache.is_none());
438 assert!(live.response_cache.is_some());
439 }
440}