use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::sync::Mutex as StdMutex; use std::time::Duration; use aether_data::repository::proxy_nodes::{ ProxyNodeEventQuery, ProxyNodeHeartbeatMutation, ProxyNodeManualCreateMutation, ProxyNodeManualUpdateMutation, ProxyNodeMetricsStep, ProxyNodeTrafficMutation, ProxyNodeTunnelStatusMutation, StoredProxyFleetMetricsBucket, StoredProxyNode, StoredProxyNodeEvent, StoredProxyNodeMetricsBucket, }; use aether_http::{build_http_client, HttpClientConfig}; use aether_runtime::{ service_up_sample, AdmissionPermit, ConcurrencyGate, ConcurrencySnapshot, MetricKind, MetricLabel, MetricSample, }; use aether_runtime_state::{ MemoryRuntimeStateConfig, RuntimeQueueStore, RuntimeSemaphore, RuntimeSemaphoreError, RuntimeSemaphoreSnapshot, RuntimeState, }; use aether_scheduler_core::PROVIDER_KEY_RPM_WINDOW_SECS; use super::{AppState, FrontdoorCorsConfig, LocalExecutionRuntimeMissDiagnostic}; use super::super::async_task::{ spawn_video_task_poller, VideoTaskPollerConfig, VideoTaskService, VideoTaskTruthSourceMode, }; use super::super::cache::{ AuthApiKeyLastUsedCache, AuthContextCache, DashboardResponseCache, DirectPlanBypassCache, SchedulerAffinityCache, SchedulerAffinitySnapshotEntry, SchedulerAffinityTarget, SystemConfigCache, }; use super::super::data::{GatewayDataConfig, GatewayDataState}; use super::super::fallback_metrics; use super::super::fallback_metrics::{GatewayFallbackMetricKind, GatewayFallbackReason}; use super::super::model_fetch::spawn_model_fetch_worker; use super::super::rate_limit::{FrontdoorUserRpmConfig, FrontdoorUserRpmLimiter}; use super::super::router::RequestAdmissionError; use super::super::{control::GatewayControlDecision, error::GatewayError}; use super::super::{provider_transport, usage}; use crate::maintenance::spawn_account_self_check_worker; use crate::maintenance::spawn_audit_cleanup_worker; use crate::maintenance::spawn_db_maintenance_worker; use crate::maintenance::spawn_gemini_file_mapping_cleanup_worker; use crate::maintenance::spawn_oauth_token_refresh_worker; use crate::maintenance::spawn_pending_cleanup_worker; use crate::maintenance::spawn_pool_monitor_worker; use crate::maintenance::spawn_pool_score_rebuild_worker; use crate::maintenance::spawn_provider_checkin_worker; use crate::maintenance::spawn_proxy_node_metrics_cleanup_worker; use crate::maintenance::spawn_proxy_node_stale_cleanup_worker; use crate::maintenance::spawn_proxy_upgrade_rollout_worker; use crate::maintenance::spawn_request_candidate_cleanup_worker; use crate::maintenance::spawn_stats_aggregation_worker; use crate::maintenance::spawn_stats_hourly_aggregation_worker; use crate::maintenance::spawn_usage_cleanup_worker; use crate::maintenance::spawn_usage_counter_flush_worker; use crate::maintenance::spawn_wallet_daily_usage_aggregation_worker; const SYSTEM_CONFIG_CACHE_TTL: Duration = Duration::from_secs(3); const SCHEDULER_AFFECTING_SYSTEM_CONFIG_KEYS: &[&str] = &[ "enable_format_conversion", "keep_priority_on_conversion", "provider_priority_mode", "scheduling_mode", ]; const AUTH_AFFECTING_SYSTEM_CONFIG_KEYS: &[&str] = &[crate::constants::DEFAULT_USER_GROUP_CONFIG_KEY]; const FRONTDOOR_RPM_AFFECTING_SYSTEM_CONFIG_KEYS: &[&str] = &["rate_limit_per_minute"]; fn system_config_key_affects_scheduler(key: &str) -> bool { let key = key.trim(); SCHEDULER_AFFECTING_SYSTEM_CONFIG_KEYS.contains(&key) } fn system_config_key_affects_auth(key: &str) -> bool { let key = key.trim(); AUTH_AFFECTING_SYSTEM_CONFIG_KEYS.contains(&key) } fn system_config_key_affects_frontdoor_rpm(key: &str) -> bool { let key = key.trim(); FRONTDOOR_RPM_AFFECTING_SYSTEM_CONFIG_KEYS.contains(&key) } impl AppState { fn usage_worker_queue_for( runtime_state: &Arc, ) -> Option> { if runtime_state.is_redis() { let queue: Arc = runtime_state.clone(); Some(queue) } else { None } } fn spawn_scheduler_affinity_redis_write( &self, cache_key: &str, target: &SchedulerAffinityTarget, ttl: Duration, epoch: u64, ) { if self.runtime_state.is_memory() { return; } let Ok(handle) = tokio::runtime::Handle::try_current() else { return; }; let cache_key = cache_key.to_string(); let runtime_state = self.runtime_state.clone(); let scheduler_affinity_epoch = self.scheduler_affinity_epoch.clone(); let provider_id = target.provider_id.clone(); let endpoint_id = target.endpoint_id.clone(); let key_id = target.key_id.clone(); let ttl_seconds = ttl.as_secs(); let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64; let expire_at = now_unix_secs.saturating_add(ttl_seconds); handle.spawn(async move { if scheduler_affinity_epoch.load(Ordering::Acquire) != epoch { return; } let existing = runtime_state .kv_get(&cache_key) .await .ok() .flatten() .and_then(|raw| serde_json::from_str::(&raw).ok()); let request_count = existing .as_ref() .and_then(|value| value.get("request_count")) .and_then(serde_json::Value::as_u64) .unwrap_or_default() .saturating_add(1); let created_at = existing .as_ref() .and_then(|value| value.get("created_at")) .and_then(serde_json::Value::as_u64) .unwrap_or(now_unix_secs); let payload = serde_json::json!({ "provider_id": provider_id, "endpoint_id": endpoint_id, "key_id": key_id, "created_at": created_at, "expire_at": expire_at, "request_count": request_count, "scheduler_affinity_epoch": epoch, }); if let Ok(serialized) = serde_json::to_string(&payload) { if scheduler_affinity_epoch.load(Ordering::Acquire) != epoch { return; } let _ = runtime_state .kv_set( &cache_key, serialized, Some(Duration::from_secs(ttl_seconds)), ) .await; } }); } pub(crate) fn replace_data_state(&mut self, data: Arc) { self.clear_provider_transport_snapshot_cache(); self.invalidate_scheduler_affinity_cache(); self.invalidate_auth_context_cache(); self.system_config_cache.clear(); self.frontdoor_user_rpm.clear_system_default_cache(); let data = Arc::new( (*data) .clone() .with_usage_worker_queue(Self::usage_worker_queue_for(&self.runtime_state)), ); self.tunnel = crate::tunnel::EmbeddedTunnelState::with_data_and_runtime_state( Arc::clone(&data), self.runtime_state.clone(), ); self.data = data; } pub fn force_close_all_tunnel_proxies(&self) -> usize { self.tunnel.request_close_all_proxies() } pub fn new() -> Result { Self::build(None) } #[cfg(test)] pub(crate) fn with_execution_runtime_override_base_url( mut self, execution_runtime_override_base_url: impl Into, ) -> Self { self.execution_runtime_override_base_url = Some( execution_runtime_override_base_url .into() .trim_end_matches('/') .to_string(), ) .filter(|value| !value.is_empty()); self } fn build(execution_runtime_override_base_url: Option) -> Result { let runtime_state = Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default())); let data = Arc::new( GatewayDataState::disabled() .with_usage_worker_queue(Self::usage_worker_queue_for(&runtime_state)), ); let client = build_http_client(&HttpClientConfig { connect_timeout_ms: Some(10_000), request_timeout_ms: Some(300_000), http2_adaptive_window: true, ..HttpClientConfig::default() })?; Ok(Self { #[cfg(test)] execution_runtime_override_base_url: execution_runtime_override_base_url .map(|value| value.trim_end_matches('/').to_string()) .filter(|value| !value.is_empty()), #[cfg(test)] execution_runtime_sync_override: None, data: Arc::clone(&data), runtime_state: runtime_state.clone(), usage_runtime: Arc::new(usage::UsageRuntime::disabled()), video_tasks: Arc::new(VideoTaskService::new( VideoTaskTruthSourceMode::PythonSyncReport, )), video_task_poller: None, request_gate: None, distributed_request_gate: None, client, auth_context_cache: Arc::new(AuthContextCache::default()), auth_api_key_last_used_cache: Arc::new(AuthApiKeyLastUsedCache::default()), oauth_refresh: Arc::new(provider_transport::LocalOAuthRefreshCoordinator::new()), direct_plan_bypass_cache: Arc::new(DirectPlanBypassCache::default()), scheduler_affinity_cache: Arc::new(SchedulerAffinityCache::default()), scheduler_affinity_epoch: Arc::new(AtomicU64::new(0)), dashboard_response_cache: Arc::new(DashboardResponseCache::default()), system_config_cache: Arc::new(SystemConfigCache::default()), fallback_metrics: Arc::new(fallback_metrics::GatewayFallbackMetrics::default()), frontdoor_cors: None, frontdoor_user_rpm: Arc::new(FrontdoorUserRpmLimiter::new( FrontdoorUserRpmConfig::default(), )), tunnel: crate::tunnel::EmbeddedTunnelState::with_data_and_runtime_state( data, runtime_state.clone(), ), provider_transport_snapshot_cache: Arc::new(StdMutex::new(HashMap::new())), provider_key_rpm_resets: Arc::new(StdMutex::new(HashMap::new())), local_execution_runtime_miss_diagnostics: Arc::new(StdMutex::new(HashMap::new())), admin_monitoring_error_stats_reset_at: Arc::new(StdMutex::new(None)), provider_delete_tasks: Arc::new(StdMutex::new(HashMap::new())), #[cfg(test)] turnstile_siteverify_url_override: None, #[cfg(test)] turnstile_siteverify_timeout_override: None, #[cfg(test)] provider_oauth_state_store: None, #[cfg(test)] provider_oauth_device_session_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] provider_oauth_batch_task_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] auth_session_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] auth_email_verification_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] auth_email_delivery_store: Some(Arc::new(StdMutex::new(Vec::new()))), #[cfg(test)] auth_user_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] auth_user_model_capability_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] auth_wallet_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] admin_wallet_payment_order_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] admin_payment_callback_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] admin_wallet_transaction_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] admin_wallet_refund_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] admin_billing_rule_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] admin_billing_collector_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] admin_security_blacklist_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] admin_security_whitelist_store: Some(Arc::new(StdMutex::new( std::collections::BTreeSet::new(), ))), #[cfg(test)] admin_monitoring_cache_affinity_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] admin_monitoring_redis_key_store: Some(Arc::new(StdMutex::new(HashMap::new()))), #[cfg(test)] provider_oauth_token_url_overrides: Arc::new(StdMutex::new(HashMap::new())), }) } pub const fn execution_runtime_configured(&self) -> bool { true } #[cfg(test)] pub(crate) fn execution_runtime_override_base_url(&self) -> Option<&str> { self.execution_runtime_override_base_url.as_deref() } pub fn with_data_config( mut self, config: GatewayDataConfig, ) -> Result { self.replace_data_state(Arc::new(GatewayDataState::from_config(config)?)); Ok(self) } pub fn with_tunnel_identity( mut self, instance_id: impl Into, relay_base_url: Option>, ) -> Self { self.tunnel = crate::tunnel::EmbeddedTunnelState::with_data_identity_and_runtime_state( Arc::clone(&self.data), instance_id, relay_base_url, 90, self.runtime_state.clone(), ); self } pub fn with_video_task_truth_source_mode(mut self, mode: VideoTaskTruthSourceMode) -> Self { self.video_tasks = Arc::new(self.video_tasks.with_truth_source_mode(mode)); self } pub fn with_usage_runtime_config( mut self, config: usage::UsageRuntimeConfig, ) -> Result { self.usage_runtime = Arc::new(usage::UsageRuntime::new(config)?); Ok(self) } pub async fn run_database_migrations(&self) -> Result { self.data.run_database_migrations().await } pub async fn run_database_backfills(&self) -> Result { self.data.run_database_backfills().await } pub async fn pending_database_migrations( &self, ) -> Result< Option>, sqlx::migrate::MigrateError, > { self.data.pending_database_migrations().await } pub async fn prepare_database_for_startup( &self, ) -> Result< Option>, sqlx::migrate::MigrateError, > { self.data.prepare_database_for_startup().await } pub async fn pending_database_backfills( &self, ) -> Result< Option>, sqlx::migrate::MigrateError, > { self.data.pending_database_backfills().await } pub fn with_video_task_poller_config(mut self, interval: Duration, batch_size: usize) -> Self { self.video_task_poller = Some(VideoTaskPollerConfig { interval, batch_size: batch_size.max(1), }); self } pub fn with_request_concurrency_limit(mut self, limit: usize) -> Self { self.request_gate = Some(Arc::new(ConcurrencyGate::new( "gateway_requests", limit.max(1), ))); self } pub fn with_runtime_state(mut self, runtime_state: Arc) -> Self { self.runtime_state = runtime_state; self.data = Arc::new( (*self.data) .clone() .with_usage_worker_queue(Self::usage_worker_queue_for(&self.runtime_state)), ); self.tunnel = crate::tunnel::EmbeddedTunnelState::with_data_and_runtime_state( Arc::clone(&self.data), self.runtime_state.clone(), ); self } pub fn with_distributed_request_concurrency_gate(mut self, gate: RuntimeSemaphore) -> Self { self.distributed_request_gate = Some(Arc::new(gate)); self } pub fn with_frontdoor_cors_config(mut self, config: FrontdoorCorsConfig) -> Self { self.frontdoor_cors = Some(Arc::new(config)); self } pub fn with_frontdoor_user_rpm_config(mut self, config: FrontdoorUserRpmConfig) -> Self { self.frontdoor_user_rpm = Arc::new(FrontdoorUserRpmLimiter::new(config)); self } pub fn has_data_backends(&self) -> bool { self.data.has_backends() } pub(crate) fn has_auth_api_key_reader(&self) -> bool { self.data.has_auth_api_key_reader() } pub(crate) fn has_proxy_node_reader(&self) -> bool { self.data.has_proxy_node_reader() } pub(crate) fn has_proxy_node_writer(&self) -> bool { self.data.has_proxy_node_writer() } pub(crate) fn frontdoor_cors(&self) -> Option> { self.frontdoor_cors.clone() } pub(crate) fn frontdoor_user_rpm(&self) -> Arc { Arc::clone(&self.frontdoor_user_rpm) } pub(crate) fn mark_provider_key_rpm_reset(&self, key_id: &str, now_unix_secs: u64) { let mut resets = self .provider_key_rpm_resets .lock() .expect("provider key rpm reset cache should lock"); let min_kept = now_unix_secs.saturating_sub(PROVIDER_KEY_RPM_WINDOW_SECS); resets.retain(|_, reset_at| *reset_at >= min_kept); resets.insert(key_id.to_string(), now_unix_secs); } pub(crate) fn provider_key_rpm_reset_at( &self, key_id: &str, now_unix_secs: u64, ) -> Option { let mut resets = self .provider_key_rpm_resets .lock() .expect("provider key rpm reset cache should lock"); let min_kept = now_unix_secs.saturating_sub(PROVIDER_KEY_RPM_WINDOW_SECS); resets.retain(|_, reset_at| *reset_at >= min_kept); resets.get(key_id).copied() } pub(crate) fn admin_monitoring_error_stats_reset_at(&self) -> Option { *self .admin_monitoring_error_stats_reset_at .lock() .expect("admin monitoring error stats reset cache should lock") } pub(crate) fn mark_admin_monitoring_error_stats_reset(&self, now_unix_secs: u64) { let mut reset_at = self .admin_monitoring_error_stats_reset_at .lock() .expect("admin monitoring error stats reset cache should lock"); *reset_at = Some(now_unix_secs); } pub(crate) async fn read_system_config_json_value( &self, key: &str, ) -> Result, GatewayError> { if let Some(value) = self.system_config_cache.get(key, SYSTEM_CONFIG_CACHE_TTL) { return Ok(value); } let value = self .data .find_system_config_value(key) .await .map_err(|err| GatewayError::Internal(err.to_string()))?; self.system_config_cache .insert(key.to_string(), value.clone(), SYSTEM_CONFIG_CACHE_TTL); Ok(value) } pub(crate) async fn upsert_system_config_json_value( &self, key: &str, value: &serde_json::Value, description: Option<&str>, ) -> Result { let value = self .data .upsert_system_config_value(key, value, description) .await .map_err(|err| GatewayError::Internal(err.to_string()))?; self.remember_system_config_write(key, Some(value.clone())); Ok(value) } pub(crate) async fn list_system_config_entries( &self, ) -> Result, GatewayError> { self.data .list_system_config_entries() .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn upsert_system_config_entry( &self, key: &str, value: &serde_json::Value, description: Option<&str>, ) -> Result { let entry = self .data .upsert_system_config_entry(key, value, description) .await .map_err(|err| GatewayError::Internal(err.to_string()))?; self.remember_system_config_write(entry.key.as_str(), Some(entry.value.clone())); Ok(entry) } pub(crate) async fn delete_system_config_value(&self, key: &str) -> Result { let deleted = self .data .delete_system_config_value(key) .await .map_err(|err| GatewayError::Internal(err.to_string()))?; self.system_config_cache .insert(key.to_string(), None, SYSTEM_CONFIG_CACHE_TTL); if deleted && system_config_key_affects_scheduler(key) { self.invalidate_scheduler_affinity_cache(); } if deleted && system_config_key_affects_auth(key) { self.invalidate_auth_context_cache(); } if deleted && system_config_key_affects_frontdoor_rpm(key) { self.frontdoor_user_rpm.clear_system_default_cache(); } Ok(deleted) } pub(crate) fn invalidate_provider_routing_caches(&self) { self.data.clear_minimal_candidate_selection_cache(); self.clear_provider_transport_snapshot_cache(); self.invalidate_scheduler_affinity_cache(); } pub(crate) fn invalidate_auth_context_cache(&self) { self.auth_context_cache.clear(); } fn remember_system_config_write(&self, key: &str, value: Option) { self.system_config_cache .insert(key.to_string(), value, SYSTEM_CONFIG_CACHE_TTL); if system_config_key_affects_scheduler(key) { self.invalidate_scheduler_affinity_cache(); } if system_config_key_affects_auth(key) { self.invalidate_auth_context_cache(); } if system_config_key_affects_frontdoor_rpm(key) { self.frontdoor_user_rpm.clear_system_default_cache(); } } pub(crate) async fn read_admin_system_stats( &self, ) -> Result { self.data .read_admin_system_stats() .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn purge_admin_system_data( &self, target: aether_data::repository::system::AdminSystemPurgeTarget, ) -> Result { let summary = self .data .purge_admin_system_data(target) .await .map_err(|err| GatewayError::Internal(err.to_string()))?; if matches!( target, aether_data::repository::system::AdminSystemPurgeTarget::Config | aether_data::repository::system::AdminSystemPurgeTarget::Users | aether_data::repository::system::AdminSystemPurgeTarget::Usage | aether_data::repository::system::AdminSystemPurgeTarget::Stats ) { self.system_config_cache.clear(); self.invalidate_provider_routing_caches(); } Ok(summary) } pub(crate) async fn run_admin_system_cleanup_once( &self, ) -> Result { crate::maintenance::run_admin_system_cleanup_once(&self.data) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn rebuild_admin_stats_once( &self, ) -> Result { crate::maintenance::rebuild_admin_stats_once(&self.data) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn find_proxy_node( &self, node_id: &str, ) -> Result, GatewayError> { self.data .find_proxy_node(node_id) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn list_proxy_nodes(&self) -> Result, GatewayError> { self.data .list_proxy_nodes() .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn list_proxy_node_events( &self, node_id: &str, limit: usize, ) -> Result, GatewayError> { self.data .list_proxy_node_events(node_id, limit) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn list_proxy_node_events_filtered( &self, node_id: &str, query: &ProxyNodeEventQuery, ) -> Result, GatewayError> { self.data .list_proxy_node_events_filtered(node_id, query) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn list_proxy_node_metrics( &self, node_id: &str, step: ProxyNodeMetricsStep, from_unix_secs: u64, to_unix_secs: u64, limit: usize, ) -> Result, GatewayError> { self.data .list_proxy_node_metrics(node_id, step, from_unix_secs, to_unix_secs, limit) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn list_proxy_fleet_metrics( &self, step: ProxyNodeMetricsStep, from_unix_secs: u64, to_unix_secs: u64, limit: usize, ) -> Result, GatewayError> { self.data .list_proxy_fleet_metrics(step, from_unix_secs, to_unix_secs, limit) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn register_proxy_node( &self, mutation: &aether_data::repository::proxy_nodes::ProxyNodeRegistrationMutation, ) -> Result, GatewayError> { self.data .register_proxy_node(mutation) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn create_manual_proxy_node( &self, mutation: &ProxyNodeManualCreateMutation, ) -> Result, GatewayError> { self.data .create_manual_proxy_node(mutation) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn update_manual_proxy_node( &self, mutation: &ProxyNodeManualUpdateMutation, ) -> Result, GatewayError> { self.data .update_manual_proxy_node(mutation) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub async fn reset_stale_proxy_node_tunnel_statuses(&self) -> std::io::Result { self.data .reset_stale_proxy_node_tunnel_statuses() .await .map_err(|err| std::io::Error::other(err.to_string())) } pub(crate) async fn cleanup_proxy_node_metrics( &self, retain_1m_from_unix_secs: u64, retain_1h_from_unix_secs: u64, delete_limit: usize, ) -> Result { self.data .cleanup_proxy_node_metrics( retain_1m_from_unix_secs, retain_1h_from_unix_secs, delete_limit, ) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn apply_proxy_node_heartbeat( &self, mutation: &ProxyNodeHeartbeatMutation, ) -> Result, GatewayError> { self.data .apply_proxy_node_heartbeat(mutation) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn record_proxy_node_traffic( &self, mutation: &ProxyNodeTrafficMutation, ) -> Result { self.data .record_proxy_node_traffic(mutation) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn unregister_proxy_node( &self, node_id: &str, ) -> Result, GatewayError> { self.data .unregister_proxy_node(node_id) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn delete_proxy_node( &self, node_id: &str, ) -> Result, GatewayError> { self.data .delete_proxy_node(node_id) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn update_proxy_node_remote_config( &self, mutation: &aether_data::repository::proxy_nodes::ProxyNodeRemoteConfigMutation, ) -> Result, GatewayError> { self.data .update_proxy_node_remote_config(mutation) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn update_proxy_node_tunnel_status( &self, mutation: &ProxyNodeTunnelStatusMutation, ) -> Result, GatewayError> { self.data .update_proxy_node_tunnel_status(mutation) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) fn request_concurrency_snapshot(&self) -> Option { self.request_gate.as_ref().map(|gate| gate.snapshot()) } pub(crate) async fn distributed_request_concurrency_snapshot( &self, ) -> Result, RuntimeSemaphoreError> { match self.distributed_request_gate.as_ref() { Some(gate) => gate.snapshot().await.map(Some), None => Ok(None), } } pub(crate) async fn metric_samples(&self) -> Vec { let mut samples = vec![service_up_sample("aether-gateway")]; if let Some(snapshot) = self.request_concurrency_snapshot() { samples.extend(snapshot.to_metric_samples("gateway_requests")); } if let Some(gate) = self.distributed_request_gate.as_ref() { match gate.snapshot().await { Ok(snapshot) => { samples.extend(snapshot.to_metric_samples("gateway_requests_distributed")); } Err(_) => samples.push( MetricSample::new( "concurrency_unavailable", "Whether the distributed concurrency gate is currently unavailable.", MetricKind::Gauge, 1, ) .with_labels(vec![MetricLabel::new( "gate", "gateway_requests_distributed", )]), ), } } samples.extend(self.tunnel.metric_samples()); samples.extend(self.fallback_metrics.metric_samples()); samples } pub(crate) fn record_fallback_metric( &self, kind: GatewayFallbackMetricKind, decision: Option<&GatewayControlDecision>, plan_kind: Option<&str>, execution_path: Option<&str>, reason: GatewayFallbackReason, ) { self.fallback_metrics .record(kind, decision, plan_kind, execution_path, reason); } pub(crate) fn clear_local_execution_runtime_miss_diagnostic(&self, trace_id: &str) { self.local_execution_runtime_miss_diagnostics .lock() .expect("local execution runtime miss diagnostics should lock") .remove(trace_id); } pub(crate) fn set_local_execution_runtime_miss_diagnostic( &self, trace_id: &str, diagnostic: LocalExecutionRuntimeMissDiagnostic, ) { let mut diagnostics = self .local_execution_runtime_miss_diagnostics .lock() .expect("local execution runtime miss diagnostics should lock"); if diagnostics .get(trace_id) .is_some_and(|existing| should_preserve_runtime_miss_diagnostic(existing, &diagnostic)) { return; } diagnostics.insert(trace_id.to_string(), diagnostic); } pub(crate) fn mutate_local_execution_runtime_miss_diagnostic( &self, trace_id: &str, mutate: F, ) where F: FnOnce(&mut LocalExecutionRuntimeMissDiagnostic), { let mut diagnostics = self .local_execution_runtime_miss_diagnostics .lock() .expect("local execution runtime miss diagnostics should lock"); if let Some(diagnostic) = diagnostics.get_mut(trace_id) { mutate(diagnostic); } } pub(crate) fn local_execution_runtime_miss_diagnostic_has_candidate_signal( &self, trace_id: &str, ) -> bool { self.local_execution_runtime_miss_diagnostics .lock() .expect("local execution runtime miss diagnostics should lock") .get(trace_id) .is_some_and(runtime_miss_diagnostic_has_candidate_signal) } pub(crate) fn take_local_execution_runtime_miss_diagnostic( &self, trace_id: &str, ) -> Option { self.local_execution_runtime_miss_diagnostics .lock() .expect("local execution runtime miss diagnostics should lock") .remove(trace_id) } pub(crate) async fn try_acquire_request_permit( &self, ) -> Result, RequestAdmissionError> { let local = self .request_gate .as_ref() .map(|gate| gate.try_acquire()) .transpose() .map_err(RequestAdmissionError::Local)?; let distributed = match self.distributed_request_gate.as_ref() { Some(gate) => Some( gate.try_acquire() .await .map_err(RequestAdmissionError::Distributed)?, ), None => None, }; Ok(AdmissionPermit::from_parts(local, distributed)) } pub fn has_auth_api_key_data_reader(&self) -> bool { self.data.has_auth_api_key_reader() } pub fn has_gemini_file_mapping_data_reader(&self) -> bool { self.data.has_gemini_file_mapping_reader() } pub fn has_gemini_file_mapping_data_writer(&self) -> bool { self.data.has_gemini_file_mapping_writer() } pub fn has_redis_data_backend(&self) -> bool { self.runtime_state.is_redis() } pub(crate) fn runtime_state_backend(&self) -> &'static str { self.runtime_state.backend_kind().as_str() } pub fn runtime_state(&self) -> &RuntimeState { self.runtime_state.as_ref() } pub(crate) async fn runtime_kv_setex( &self, key: &str, value: &str, ttl_seconds: u64, ) -> Result<(), GatewayError> { self.runtime_state .kv_set( key, value.to_string(), Some(Duration::from_secs(ttl_seconds)), ) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn runtime_kv_get(&self, key: &str) -> Result, GatewayError> { self.runtime_state .kv_get(key) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn runtime_kv_getdel( &self, key: &str, ) -> Result, GatewayError> { self.runtime_state .kv_take(key) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn runtime_kv_del(&self, key: &str) -> Result { self.runtime_state .kv_delete(key) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) async fn runtime_kv_exists(&self, key: &str) -> Result { self.runtime_state .kv_exists(key) .await .map_err(|err| GatewayError::Internal(err.to_string())) } pub(crate) fn remove_scheduler_affinity_cache_entry(&self, cache_key: &str) -> bool { self.scheduler_affinity_cache.remove(cache_key).is_some() } pub(crate) fn scheduler_affinity_epoch(&self) -> u64 { self.scheduler_affinity_epoch.load(Ordering::Acquire) } pub(crate) fn invalidate_scheduler_affinity_cache(&self) -> u64 { let next_epoch = self .scheduler_affinity_epoch .fetch_add(1, Ordering::AcqRel) .saturating_add(1); self.scheduler_affinity_cache.clear(); next_epoch } pub(crate) fn read_scheduler_affinity_target( &self, cache_key: &str, ttl: Duration, ) -> Option { self.scheduler_affinity_cache.get_fresh_for_epoch( cache_key, ttl, self.scheduler_affinity_epoch(), ) } pub(crate) fn remember_scheduler_affinity_target( &self, cache_key: &str, target: SchedulerAffinityTarget, ttl: Duration, max_entries: usize, ) { let epoch = self.scheduler_affinity_epoch(); self.remember_scheduler_affinity_target_for_epoch( cache_key, target, ttl, max_entries, Some(epoch), ); } pub(crate) fn remember_scheduler_affinity_target_for_epoch( &self, cache_key: &str, target: SchedulerAffinityTarget, ttl: Duration, max_entries: usize, expected_epoch: Option, ) -> bool { let epoch = expected_epoch.unwrap_or_else(|| self.scheduler_affinity_epoch()); if self.scheduler_affinity_epoch() != epoch { return false; } self.spawn_scheduler_affinity_redis_write(cache_key, &target, ttl, epoch); self.scheduler_affinity_cache.insert_for_epoch( cache_key.to_string(), target, ttl, max_entries, epoch, ); true } pub(crate) fn list_scheduler_affinity_entries( &self, ttl: Duration, ) -> Vec { self.scheduler_affinity_cache .fresh_entries_for_epoch(ttl, self.scheduler_affinity_epoch()) } pub fn with_video_task_store_path( mut self, path: impl Into, ) -> std::io::Result { self.video_tasks = Arc::new(VideoTaskService::with_file_store( self.video_tasks.truth_source_mode(), path, )?); Ok(self) } pub fn spawn_background_tasks(&self) -> crate::task_runtime::TaskSupervisor { let mut supervisor = crate::task_runtime::TaskSupervisor::new(); let record_boot = |task_key: &'static str| { if !self.has_background_task_data_writer() { return; } let Some(definition) = crate::task_runtime::task_definition(task_key) else { return; }; std::mem::drop(crate::task_runtime::spawn_record_worker_boot( self.clone(), task_key, crate::task_runtime::background_task_kind(definition.kind), definition.trigger, )); }; let mut supervise_worker = |task_key: &'static str, handle: Option>| { if let Some(handle) = handle { supervisor.supervise_handle(task_key, handle); record_boot(task_key); } }; supervise_worker( crate::task_runtime::TASK_KEY_USAGE_QUEUE_WORKER, self.usage_runtime.spawn_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_USAGE_COUNTER_FLUSH, spawn_usage_counter_flush_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_PROVIDER_QUOTA_RESET, crate::wallet_runtime::spawn_provider_quota_reset_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_AUDIT_CLEANUP, spawn_audit_cleanup_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_DB_MAINTENANCE, spawn_db_maintenance_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_WALLET_DAILY_USAGE_AGG, spawn_wallet_daily_usage_aggregation_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_STATS_DAILY_AGG, spawn_stats_aggregation_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_USAGE_CLEANUP, spawn_usage_cleanup_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_POOL_MONITOR, spawn_pool_monitor_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_ACCOUNT_SELF_CHECK, spawn_account_self_check_worker(self.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_POOL_SCORE_REBUILD, spawn_pool_score_rebuild_worker(self.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_STATS_HOURLY_AGG, spawn_stats_hourly_aggregation_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_PENDING_CLEANUP, spawn_pending_cleanup_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_PROXY_NODE_STALE_CLEANUP, spawn_proxy_node_stale_cleanup_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_PROXY_NODE_METRICS_CLEANUP, spawn_proxy_node_metrics_cleanup_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_PROXY_UPGRADE_ROLLOUT, spawn_proxy_upgrade_rollout_worker(self.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_PROVIDER_CHECKIN, spawn_provider_checkin_worker(self.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_OAUTH_TOKEN_REFRESH, spawn_oauth_token_refresh_worker(self.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_REQUEST_CANDIDATE_CLEANUP, spawn_request_candidate_cleanup_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_GEMINI_FILES_CLEANUP, spawn_gemini_file_mapping_cleanup_worker(self.data.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_MODEL_FETCH_WORKER, spawn_model_fetch_worker(self.clone()), ); supervise_worker( crate::task_runtime::TASK_KEY_VIDEO_TASK_POLLER, spawn_video_task_poller(self.clone()), ); supervisor } } fn should_preserve_runtime_miss_diagnostic( existing: &LocalExecutionRuntimeMissDiagnostic, next: &LocalExecutionRuntimeMissDiagnostic, ) -> bool { runtime_miss_diagnostic_has_candidate_signal(existing) && !runtime_miss_diagnostic_has_candidate_signal(next) } fn runtime_miss_diagnostic_has_candidate_signal( diagnostic: &LocalExecutionRuntimeMissDiagnostic, ) -> bool { diagnostic.candidate_count.unwrap_or(0) > 0 || diagnostic.skipped_candidate_count.unwrap_or(0) > 0 || !diagnostic.skip_reasons.is_empty() } #[cfg(test)] mod tests { use std::sync::Arc; use serde_json::json; use super::AppState; use crate::cache::SchedulerAffinityTarget; use crate::data::GatewayDataState; #[tokio::test] async fn system_config_reads_use_short_lived_cache_until_app_invalidation() { let state = AppState::new() .expect("app state should build") .with_data_state_for_tests( GatewayDataState::disabled() .with_system_config_values_for_tests([("site_name".to_string(), json!("old"))]), ); assert_eq!( state .read_system_config_json_value("site_name") .await .expect("system config read should succeed"), Some(json!("old")) ); state .data .upsert_system_config_value("site_name", &json!("bypassed"), None) .await .expect("direct data write should succeed"); assert_eq!( state .read_system_config_json_value("site_name") .await .expect("cached system config read should succeed"), Some(json!("old")) ); state .upsert_system_config_json_value("site_name", &json!("fresh"), None) .await .expect("app system config write should succeed"); assert_eq!( state .read_system_config_json_value("site_name") .await .expect("refreshed system config read should succeed"), Some(json!("fresh")) ); } #[tokio::test] async fn system_config_entry_write_refreshes_cache_and_scheduler_affinity_for_routing_keys() { let state = AppState::new() .expect("app state should build") .with_data_state_for_tests( GatewayDataState::disabled().with_system_config_values_for_tests([( "keep_priority_on_conversion".to_string(), json!(false), )]), ); let cache_key = "scheduler_affinity:api-key-1:openai:chat:gpt-5"; let ttl = std::time::Duration::from_secs(300); assert_eq!( state .read_system_config_json_value("keep_priority_on_conversion") .await .expect("system config read should succeed"), Some(json!(false)) ); state.remember_scheduler_affinity_target( cache_key, SchedulerAffinityTarget { provider_id: "provider-old".to_string(), endpoint_id: "endpoint-old".to_string(), key_id: "key-old".to_string(), }, ttl, 128, ); assert!(state .read_scheduler_affinity_target(cache_key, ttl) .is_some()); let initial_epoch = state.scheduler_affinity_epoch(); state .upsert_system_config_entry("keep_priority_on_conversion", &json!(true), None) .await .expect("admin config write should succeed"); assert_eq!( state .read_system_config_json_value("keep_priority_on_conversion") .await .expect("system config read should use refreshed cache"), Some(json!(true)) ); assert!(state.scheduler_affinity_epoch() > initial_epoch); assert_eq!(state.read_scheduler_affinity_target(cache_key, ttl), None); } #[tokio::test] async fn system_config_write_refreshes_frontdoor_rpm_default_cache() { let state = AppState::new() .expect("app state should build") .with_data_state_for_tests( GatewayDataState::disabled().with_system_config_values_for_tests([( "rate_limit_per_minute".to_string(), json!(1), )]), ); assert_eq!( state .frontdoor_user_rpm() .current_system_default_limit(&state) .await .expect("default rpm limit should read"), 1 ); state .upsert_system_config_entry("rate_limit_per_minute", &json!(0), None) .await .expect("rpm system config should update"); assert_eq!( state .frontdoor_user_rpm() .current_system_default_limit(&state) .await .expect("default rpm limit should use refreshed value"), 0 ); } #[tokio::test] async fn replacing_data_state_clears_system_config_cache() { let mut state = AppState::new() .expect("app state should build") .with_data_state_for_tests( GatewayDataState::disabled() .with_system_config_values_for_tests([("site_name".to_string(), json!("old"))]), ); assert_eq!( state .read_system_config_json_value("site_name") .await .expect("system config read should succeed"), Some(json!("old")) ); state.replace_data_state(Arc::new( GatewayDataState::disabled() .with_system_config_values_for_tests([("site_name".to_string(), json!("new"))]), )); assert_eq!( state .read_system_config_json_value("site_name") .await .expect("system config read should reflect replaced data"), Some(json!("new")) ); } #[test] fn scheduler_affinity_epoch_blocks_stale_rewarm_after_invalidation() { let state = AppState::new().expect("app state should build"); let cache_key = "scheduler_affinity:api-key-1:openai:chat:gpt-5"; let ttl = std::time::Duration::from_secs(300); let first_target = crate::cache::SchedulerAffinityTarget { provider_id: "provider-old".to_string(), endpoint_id: "endpoint-old".to_string(), key_id: "key-old".to_string(), }; let next_target = crate::cache::SchedulerAffinityTarget { provider_id: "provider-new".to_string(), endpoint_id: "endpoint-new".to_string(), key_id: "key-new".to_string(), }; let initial_epoch = state.scheduler_affinity_epoch(); assert!(state.remember_scheduler_affinity_target_for_epoch( cache_key, first_target.clone(), ttl, 16, Some(initial_epoch), )); assert_eq!( state.read_scheduler_affinity_target(cache_key, ttl), Some(first_target) ); let next_epoch = state.invalidate_scheduler_affinity_cache(); assert!(!state.remember_scheduler_affinity_target_for_epoch( cache_key, next_target.clone(), ttl, 16, Some(initial_epoch), )); assert_eq!(state.read_scheduler_affinity_target(cache_key, ttl), None); assert!(state.remember_scheduler_affinity_target_for_epoch( cache_key, next_target.clone(), ttl, 16, Some(next_epoch), )); assert_eq!( state.read_scheduler_affinity_target(cache_key, ttl), Some(next_target) ); } }