use tracing::{debug, info, instrument, warn};
#[allow(unused_imports)]
use axum::{
extract::{DefaultBodyLimit, Path, State},
http::StatusCode,
response::IntoResponse,
routing::{any, delete, get, post},
Json, Router,
};
use serde::Serialize;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio_util::sync::CancellationToken;
use super::adaptive::AdaptiveTimeout;
use super::agent::{AgentIdentity, AgentRegistry};
use super::audit::AuditLog;
use super::batch::{BatchConfig, BatchProcessor, BatchRequest, BatchResponse};
use super::cache::CacheStore;
use super::cascade::CascadeExecutor;
use super::circuit::CircuitBreaker;
use super::coalesce::{CoalesceAction, RequestCoalescer};
use super::context::{ContextConfig, ContextManager, ContextMetadata};
use super::federated::{FallbackDecision, FederatedSearch, FederatedStats};
use super::lifecycle::ProxyError;
use super::metrics::ProxyMetrics;
#[allow(unused_imports)]
use super::middleware::{
auth_middleware, rate_limit_middleware, AuthConfig, RateLimitConfig, RateLimiter,
};
use super::pool::{LoadBalanceStrategy, PoolStats, UpstreamPool};
use super::priority::PriorityQueue;
use super::query_stats::QueryStatsTracker;
use super::refresh::QueryTrackingRefreshWorker;
#[allow(unused_imports)]
use super::retry::{RetryCondition, RetryExecutor, RetryPolicy};
use super::scope::ScopeFilter;
#[cfg(feature = "embed-api")]
use super::semantic_cache::SemanticCache;
#[cfg(feature = "embed-api")]
use super::smart_embedder::SmartEmbedder;
#[allow(unused_imports)]
use super::types::{
CacheStatus, CachedResponse, Freshness, MissReason, QueryRequest, QueryResponse,
};
use super::upstream::GenericRestAdapter;
#[cfg(feature = "embed-api")]
use super::upstream::{QueryMode, UpstreamAdapter};
use crate::config::ProxyConfig;
use super::cdc::CdcManager;
use arc_swap::{ArcSwap, ArcSwapOption};
type ReloadArc<T> = Arc<ArcSwap<T>>;
type ReloadOpt<T> = Arc<ArcSwapOption<T>>;
use super::peer::PeerManager;
mod admin;
mod batch;
mod cache;
mod context;
mod query;
pub(crate) mod query_core;
mod status;
mod web_ui;
#[derive(Debug, Clone, Serialize)]
pub struct ProxyErrorResponse {
pub error_type: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cascade_trail: Vec<CascadeStep>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CascadeStep {
pub upstream: String,
pub status: String,
pub latency_ms: u64,
}
impl ProxyErrorResponse {
pub fn new(error_type: &str, message: impl Into<String>) -> Self {
Self {
error_type: error_type.to_string(),
message: message.into(),
request_id: None,
cascade_trail: vec![],
}
}
pub fn with_request_id(mut self, id: Option<String>) -> Self {
self.request_id = id;
self
}
}
pub(super) fn extract_request_id(headers: &axum::http::HeaderMap) -> String {
headers
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_else(|| {
use std::time::SystemTime;
let ts = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("req-{:x}", ts)
})
}
fn resolve_context(headers: &axum::http::HeaderMap, agent: Option<&AgentIdentity>) -> String {
if let Some(val) = headers.get("x-context") {
if let Ok(ctx) = val.to_str() {
if !ctx.is_empty() {
return ctx.to_string();
}
}
}
if let Some(agent) = agent {
if let Some(ref ctx) = agent.default_context {
return ctx.clone();
}
}
"default".to_string()
}
fn context_query(context_id: &str, query: &str) -> String {
format!("ctx:{}:{}", context_id, query)
}
pub(crate) struct ClientTracker {
active: dashmap::DashMap<String, ClientInfo>,
pub(crate) total_completed: std::sync::atomic::AtomicU64,
pub(crate) total_rejected: std::sync::atomic::AtomicU64,
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct ClientInfo {
pub(crate) request_id: String,
pub(crate) started_at_ms: u64,
pub(crate) query: String,
pub(crate) source: String,
}
impl ClientTracker {
fn new() -> Self {
Self {
active: dashmap::DashMap::new(),
total_completed: std::sync::atomic::AtomicU64::new(0),
total_rejected: std::sync::atomic::AtomicU64::new(0),
}
}
fn track(&self, request_id: String, query: String, source: String) {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
self.active.insert(
request_id.clone(),
ClientInfo {
request_id,
started_at_ms: now_ms,
query,
source,
},
);
}
fn complete(&self, request_id: &str) {
self.active.remove(request_id);
self.total_completed
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
fn reject(&self) {
self.total_rejected
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn active_count(&self) -> usize {
self.active.len()
}
pub(crate) fn snapshot(&self) -> Vec<ClientInfo> {
self.active.iter().map(|r| r.value().clone()).collect()
}
}
pub struct CacheProxy {
cache: Arc<CacheStore>,
upstream: Option<Arc<GenericRestAdapter>>,
upstream_pool: Option<Arc<UpstreamPool>>,
coalescer: Arc<RequestCoalescer>,
scope_filter: Arc<ScopeFilter>,
metrics: Arc<ProxyMetrics>,
circuit_breaker: Arc<CircuitBreaker>,
audit_log: Arc<AuditLog>,
retry_policy: RetryPolicy,
adaptive_timeout: Arc<AdaptiveTimeout>,
upstream_id: String,
start_time: Instant,
refresh_interval: Duration,
api_key: Option<String>,
rate_limiter: Option<Arc<RateLimiter>>,
federated_search: Arc<FederatedSearch>,
request_queue: Arc<PriorityQueue<QueryRequest>>,
context_manager: Arc<ContextManager>,
#[cfg(feature = "embed-api")]
smart_embedder: Option<Arc<SmartEmbedder>>,
#[cfg(feature = "embed-api")]
warmup_on_start: bool,
#[cfg(feature = "embed-api")]
semantic_cache: Option<Arc<SemanticCache>>,
cascade_executor: Option<Arc<CascadeExecutor>>,
agent_registry: Option<Arc<AgentRegistry>>,
cdc_manager: Option<Arc<CdcManager>>,
peer_config: Option<crate::config::PeerConfig>,
socket_tuning: crate::config::SocketTuningConfig,
shutdown_timeout: Duration,
max_global_connections: usize,
reload_source: Option<std::path::PathBuf>,
web_ui_enabled: bool,
}
#[derive(Clone)]
pub(crate) struct AppState {
pub(crate) cache: Arc<CacheStore>,
pub(crate) upstream: ReloadOpt<GenericRestAdapter>,
pub(crate) upstream_pool: ReloadOpt<UpstreamPool>,
pub(crate) coalescer: Arc<RequestCoalescer>,
pub(crate) refresh_worker: Option<Arc<QueryTrackingRefreshWorker>>,
pub(crate) scope_filter: Arc<ScopeFilter>,
pub(crate) metrics: Arc<ProxyMetrics>,
pub(crate) circuit_breaker: Arc<CircuitBreaker>,
pub(crate) audit_log: Arc<AuditLog>,
pub(crate) retry_policy: Arc<RetryPolicy>,
pub(crate) adaptive_timeout: Arc<AdaptiveTimeout>,
pub(crate) query_stats: Arc<QueryStatsTracker>,
pub(crate) batch_processor: Arc<BatchProcessor>,
pub(crate) federated_search: ReloadArc<FederatedSearch>,
pub(crate) request_queue: Arc<PriorityQueue<QueryRequest>>,
pub(crate) upstream_id: String,
pub(crate) start_time: Instant,
pub(crate) context_manager: Arc<ContextManager>,
#[cfg(feature = "embed-api")]
pub(crate) smart_embedder: Option<Arc<SmartEmbedder>>,
#[cfg(feature = "embed-api")]
pub(crate) semantic_cache: Option<Arc<SemanticCache>>,
pub(crate) degradation_level: Arc<std::sync::atomic::AtomicU8>,
pub(crate) paused: Arc<std::sync::atomic::AtomicBool>,
pub(crate) client_tracker: Arc<ClientTracker>,
pub(crate) cascade_executor: ReloadOpt<CascadeExecutor>,
pub(crate) agent_registry: ReloadOpt<AgentRegistry>,
pub(crate) cdc_manager: Option<Arc<CdcManager>>,
pub(crate) peer_manager: Option<Arc<PeerManager>>,
pub(crate) global_concurrency: Arc<tokio::sync::Semaphore>,
pub(crate) reload_source: Option<std::path::PathBuf>,
pub(crate) tokio_handle: Option<tokio::runtime::Handle>,
}
impl AppState {
pub(crate) fn peer_is_warming(&self) -> bool {
self.peer_manager
.as_ref()
.map(|pm| !pm.is_ready())
.unwrap_or(false)
}
pub(crate) fn scope_filter_for(&self, context_id: &str) -> Arc<ScopeFilter> {
self.context_manager
.scope_filter_for(context_id)
.unwrap_or_else(|| self.scope_filter.clone())
}
pub(crate) fn apply_reload(&self) -> Result<ReloadSummary, String> {
use crate::config::Config;
let new_config = match &self.reload_source {
Some(path) => Config::load_from(
path.to_str()
.ok_or_else(|| format!("config path is not valid UTF-8: {}", path.display()))?,
)
.map_err(|e| format!("Failed to parse config at {}: {}", path.display(), e))?,
None => Config::load()
.map_err(|e| format!("Failed to load config from default locations: {}", e))?,
};
let mut sections: Vec<String> = Vec::new();
let mut restart_required: Vec<String> = Vec::new();
let projected = new_config
.config
.effective_proxy()
.map_err(|e| format!("config project: {e}"))?;
let proxy_cfg = &projected;
if let Ok(resolved) = new_config.config.resolve_contexts() {
if !resolved.is_empty() {
self.context_manager.install_resolved(&resolved);
sections.push(format!("contexts={}", resolved.len()));
}
}
let old_max = self.cache.max_entries();
let new_max = proxy_cfg.max_entries();
if new_max != old_max {
self.cache
.set_max_entries(new_max)
.map_err(|e| format!("cache.max_entries: {}", e))?;
sections.push(format!("cache.max_entries={}", new_max));
}
let new_fresh = proxy_cfg.fresh_duration_secs();
if let Some(orig_fresh) = self.cache.fresh_duration_secs() {
if new_fresh != orig_fresh {
self.cache
.set_fresh_duration(std::time::Duration::from_secs(new_fresh));
sections.push(format!("cache.fresh_duration_secs={}", new_fresh));
}
}
let new_stale = proxy_cfg.stale_duration_secs();
if let Some(orig_stale) = self.cache.stale_duration_secs() {
if new_stale != orig_stale {
self.cache
.set_stale_duration(std::time::Duration::from_secs(new_stale));
sections.push(format!("cache.stale_duration_secs={}", new_stale));
}
}
let new_federated = Arc::new(FederatedSearch::from_config(&proxy_cfg.federated));
enum UpstreamBuild {
Pool {
pool: Arc<UpstreamPool>,
cascade: Option<Arc<CascadeExecutor>>,
count: usize,
},
Single {
adapter: Arc<crate::proxy::upstream::GenericRestAdapter>,
url: String,
},
None,
}
let upstream_build = if !proxy_cfg.upstreams().is_empty() {
let strategy = crate::proxy::pool::LoadBalanceStrategy::RoundRobin;
let pool = UpstreamPool::new(proxy_cfg.upstreams(), strategy)
.map_err(|e| format!("proxy.upstreams: failed to build pool: {}", e))?;
let pool = Arc::new(pool);
let cascade = if proxy_cfg.cascade.enabled {
Some(Arc::new(CascadeExecutor::with_metrics(
pool.clone(),
proxy_cfg.cascade.clone(),
self.metrics.clone(),
)))
} else {
None
};
UpstreamBuild::Pool {
pool,
cascade,
count: proxy_cfg.upstreams().len(),
}
} else if let Some(url) = proxy_cfg.upstream_url() {
let adapter = crate::proxy::upstream::GenericRestAdapter::new(
url,
std::time::Duration::from_secs(proxy_cfg.upstream_timeout_secs()),
)
.map_err(|e| format!("proxy.upstreams: failed to build adapter: {}", e))?;
UpstreamBuild::Single {
adapter: Arc::new(adapter),
url: url.to_string(),
}
} else {
UpstreamBuild::None
};
self.federated_search.store(new_federated);
sections.push(format!(
"proxy.federated (enabled={})",
proxy_cfg.federated.enabled()
));
match upstream_build {
UpstreamBuild::Pool {
pool,
cascade,
count,
} => {
self.upstream_pool.store(Some(pool));
self.upstream.store(None);
sections.push(format!("proxy.upstreams ({} in pool)", count));
if cascade.is_some() {
self.cascade_executor.store(cascade);
sections.push("proxy.cascade".to_string());
} else {
self.cascade_executor.store(None);
}
}
UpstreamBuild::Single { adapter, url } => {
self.upstream.store(Some(adapter));
self.upstream_pool.store(None);
self.cascade_executor.store(None);
sections.push(format!("proxy.upstreams (single: {})", url));
}
UpstreamBuild::None => {
self.upstream_pool.store(None);
self.cascade_executor.store(None);
self.upstream.store(None);
sections.push("proxy.upstreams (none)".to_string());
}
}
self.circuit_breaker.set_config(
new_config
.config
.proxy
.circuit_breaker
.to_circuit_breaker_config(),
);
sections.push("proxy.circuit_breaker".to_string());
let new_agents: Vec<crate::config::AgentConfig> = if !new_config.config.agents.is_empty() {
new_config.config.agents.clone()
} else {
proxy_cfg.agents().to_vec()
};
if new_agents.is_empty() {
self.agent_registry.store(None);
sections.push("agents (none; auth disabled)".to_string());
} else {
let new_agent_registry = Arc::new(AgentRegistry::from_config(&new_agents));
self.agent_registry.store(Some(new_agent_registry.clone()));
sections.push(format!("agents ({} registered)", new_agent_registry.len()));
}
if proxy_cfg.peer().enabled() {
restart_required.push("proxy.peer (restart to apply)".to_string());
}
if let Some(ref api_key) = proxy_cfg.api_key {
if !api_key.is_empty() {
restart_required.push("proxy.api_key (auth key; restart to apply)".to_string());
}
}
if proxy_cfg.rate_limit().enabled() {
restart_required.push("proxy.rate_limit (restart to apply)".to_string());
}
if sections.is_empty() {
sections.push("config.parsed (no reloadable changes detected)".to_string());
}
Ok(ReloadSummary {
sections,
restart_required,
})
}
}
#[derive(Debug)]
#[must_use = "reload result should be checked for restart_required sections"]
pub(crate) struct ReloadSummary {
pub(crate) sections: Vec<String>,
pub(crate) restart_required: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DegradationLevel {
Full = 0,
StaleServing = 1,
ReadOnly = 2,
TextOnly = 3,
StartupFailure = 4,
}
impl DegradationLevel {
pub fn description(&self) -> &'static str {
match self {
Self::Full => "Full operation",
Self::StaleServing => "Serving from cache (upstreams degraded)",
Self::ReadOnly => "Read-only (persistence unavailable)",
Self::TextOnly => "Text-only (embedding unavailable)",
Self::StartupFailure => "Startup failure (no upstreams reachable)",
}
}
pub fn is_ready(&self) -> bool {
matches!(self, Self::Full | Self::StaleServing)
}
}
impl std::fmt::Display for DegradationLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Level {} ({})", *self as u8, self.description())
}
}
fn determine_degradation_level(state: &AppState) -> DegradationLevel {
let has_upstream =
state.upstream.load_full().is_some() || state.upstream_pool.load_full().is_some();
if !has_upstream {
return DegradationLevel::StartupFailure;
}
let pool = state.upstream_pool.load_full();
if let Some(ref pool) = pool {
let stats = pool.stats();
if stats.healthy_upstreams == 0 {
if !state.cache.is_empty() {
return DegradationLevel::StaleServing;
}
return DegradationLevel::StartupFailure;
}
}
DegradationLevel::Full
}
impl CacheProxy {
fn build_cache_store(config: &ProxyConfig) -> (Arc<CacheStore>, Option<Arc<CdcManager>>) {
let cdc_manager = if config.cdc_enabled() {
let node_id = config.peer().node_id();
Some(Arc::new(CdcManager::new(config.cdc(), node_id)))
} else {
None
};
let mut cache_store = CacheStore::with_jitter(
Duration::from_secs(config.fresh_duration_secs()),
Duration::from_secs(config.stale_duration_secs()),
config.max_entries(),
config.ttl_jitter_percent(),
)
.with_normalized_matching(config.cache.normalized_matching());
let max_memory_bytes = config
.cache
.max_memory_mb()
.saturating_mul(1024)
.saturating_mul(1024);
if max_memory_bytes > 0 {
cache_store.set_max_memory_bytes(max_memory_bytes);
}
if let Some(ref cdc) = cdc_manager {
cache_store = cache_store.with_cdc_sender(cdc.event_sender());
}
#[cfg(feature = "embed-api")]
if config.cache.semantic.enabled() {
let semantic = Arc::new(SemanticCache::new(
config.cache.semantic.similarity_threshold(),
config.cache.semantic.max_entries(),
));
cache_store = cache_store.with_semantic_cache(semantic);
}
let cache = Arc::new(cache_store);
let scope_texts = config.scope_seeds();
cache.update_config_fingerprint(config.upstream_url(), &scope_texts);
(cache, cdc_manager)
}
#[allow(clippy::type_complexity)]
fn build_upstreams(
config: &ProxyConfig,
) -> anyhow::Result<(Option<Arc<GenericRestAdapter>>, Option<Arc<UpstreamPool>>)> {
if !config.upstreams().is_empty() {
let pool = Arc::new(
UpstreamPool::new(config.upstreams(), LoadBalanceStrategy::RoundRobin)
.map_err(|e| anyhow::anyhow!("Failed to create upstream pool: {}", e))?,
);
Ok((None, Some(pool)))
} else {
let single = config
.upstream_url()
.map(|url| {
GenericRestAdapter::new(
url,
Duration::from_secs(config.upstream_timeout_secs()),
)
})
.transpose()
.map_err(|e| anyhow::anyhow!("Failed to create upstream adapter: {}", e))?
.map(Arc::new);
Ok((single, None))
}
}
fn build_resilience(config: &ProxyConfig) -> (Option<Arc<RateLimiter>>, RetryPolicy) {
let rate_limiter = if config.rate_limit().enabled() {
Some(Arc::new(RateLimiter::new(
config.rate_limit().requests_per_second(),
config.rate_limit().burst_size(),
)))
} else {
None
};
let retry_config = config.retry();
let retry_policy = if retry_config.enabled() {
RetryPolicy {
max_retries: retry_config.max_retries(),
initial_delay: Duration::from_millis(retry_config.initial_delay_ms()),
max_delay: Duration::from_millis(retry_config.max_delay_ms()),
backoff_multiplier: retry_config.backoff_multiplier(),
jitter_factor: 0.1,
retry_on: RetryCondition {
on_network_error: retry_config.on_network_error(),
on_timeout: retry_config.on_timeout(),
on_server_error: retry_config.on_server_error(),
on_rate_limited: retry_config.on_rate_limited(),
on_status_codes: vec![],
},
}
} else {
RetryPolicy::no_retry()
};
(rate_limiter, retry_policy)
}
pub fn new(config: &ProxyConfig) -> anyhow::Result<Self> {
let (cache, cdc_manager) = Self::build_cache_store(config);
let (upstream, upstream_pool) = Self::build_upstreams(config)?;
let (rate_limiter, retry_policy) = Self::build_resilience(config);
let scope_filter = Arc::new(ScopeFilter::from_config(&config.scope));
let upstream_id = format!("proxy-{}", std::process::id());
for u in config.upstreams.iter() {
if u.is_experimental() {
warn!(
upstream = %u.id,
upstream_type = ?u.upstream_type(),
"experimental upstream configured — lighter e2e coverage, \
API may drift. See README 'Supported Upstreams'."
);
}
}
#[cfg(feature = "embed-api")]
let smart_embedder = Some(Arc::new(SmartEmbedder::with_defaults()));
let metrics = Arc::new(ProxyMetrics::new());
let cascade_executor = if config.cascade.enabled {
upstream_pool.as_ref().map(|pool| {
let executor = CascadeExecutor::with_metrics(
pool.clone(),
config.cascade.clone(),
metrics.clone(),
);
#[cfg(feature = "embed-api")]
let executor = if let Some(ref embedder) = smart_embedder {
executor.with_embedder(embedder.clone())
} else {
executor
};
Arc::new(executor)
})
} else {
None
};
let peer_config = if config.peer().enabled() {
let secret_configured = matches!(config.peer().resolve_shared_secret(), Ok(Some(_)));
if secret_configured {
info!(
peers = config.peer().peers().len(),
"peer replication enabled with shared_secret auth (plan 07)"
);
} else {
warn!(
peers = config.peer().peers().len(),
"peer replication enabled without shared_secret: \
no peer-specific auth (no mTLS). Peers must be on a trusted network. \
Set [proxy.peer] shared_secret to require x-peer-secret on peer gRPC. \
Do not expose peer gRPC on the public internet."
);
}
Some(config.peer().clone())
} else {
None
};
Ok(Self {
cache,
upstream,
upstream_pool,
coalescer: Arc::new(RequestCoalescer::new()),
scope_filter,
metrics,
circuit_breaker: Arc::new(CircuitBreaker::new(
config.circuit_breaker.to_circuit_breaker_config(),
)),
audit_log: Arc::new(AuditLog::new(1000)),
retry_policy,
adaptive_timeout: Arc::new(AdaptiveTimeout::default_config()),
upstream_id,
start_time: Instant::now(),
refresh_interval: Duration::from_secs(config.refresh_interval_secs()),
api_key: config.api_key().map(|s| s.to_string()),
rate_limiter,
federated_search: Arc::new(FederatedSearch::from_config(&config.federated)),
request_queue: Arc::new(PriorityQueue::new(1000)),
context_manager: Arc::new(ContextManager::new(ContextConfig::default())),
#[cfg(feature = "embed-api")]
smart_embedder: None, #[cfg(feature = "embed-api")]
warmup_on_start: false,
#[cfg(feature = "embed-api")]
semantic_cache: None,
cascade_executor,
agent_registry: if config.has_agents() {
Some(Arc::new(AgentRegistry::from_config(config.agents())))
} else {
None
},
cdc_manager,
peer_config,
socket_tuning: config.socket_tuning.clone(),
shutdown_timeout: Duration::from_secs(config.shutdown_timeout_secs()),
max_global_connections: config.max_global_connections(),
reload_source: None,
web_ui_enabled: config.web_ui.enabled,
})
}
pub fn with_upstream(config: &ProxyConfig, upstream: GenericRestAdapter) -> Self {
let cache = Arc::new(
CacheStore::with_jitter(
Duration::from_secs(config.fresh_duration_secs()),
Duration::from_secs(config.stale_duration_secs()),
config.max_entries(),
config.ttl_jitter_percent(),
)
.with_normalized_matching(config.cache.normalized_matching()),
);
let scope_texts = config.scope_seeds();
cache.update_config_fingerprint(config.upstream_url(), &scope_texts);
let scope_filter = Arc::new(ScopeFilter::from_config(&config.scope));
let upstream_id = format!("proxy-{}", std::process::id());
let (rate_limiter, retry_policy) = Self::build_resilience(config);
let federated_search = Arc::new(FederatedSearch::from_config(&config.federated));
let request_queue = Arc::new(PriorityQueue::new(1000));
let context_manager = Arc::new(ContextManager::new(ContextConfig::default()));
Self {
cache,
upstream: Some(Arc::new(upstream)),
upstream_pool: None, coalescer: Arc::new(RequestCoalescer::new()),
scope_filter,
metrics: Arc::new(ProxyMetrics::new()),
circuit_breaker: Arc::new(CircuitBreaker::new(
config.circuit_breaker.to_circuit_breaker_config(),
)),
audit_log: Arc::new(AuditLog::new(1000)), retry_policy,
adaptive_timeout: Arc::new(AdaptiveTimeout::default_config()),
upstream_id,
start_time: Instant::now(),
refresh_interval: Duration::from_secs(config.refresh_interval_secs()),
api_key: config.api_key().map(|s| s.to_string()),
rate_limiter,
federated_search,
request_queue,
context_manager,
#[cfg(feature = "embed-api")]
smart_embedder: None, #[cfg(feature = "embed-api")]
warmup_on_start: false,
#[cfg(feature = "embed-api")]
semantic_cache: None,
cascade_executor: None, agent_registry: if config.has_agents() {
Some(Arc::new(AgentRegistry::from_config(config.agents())))
} else {
None
},
cdc_manager: None,
peer_config: None,
socket_tuning: config.socket_tuning.clone(),
shutdown_timeout: Duration::from_secs(config.shutdown_timeout_secs()),
max_global_connections: config.max_global_connections(),
reload_source: None,
web_ui_enabled: config.web_ui.enabled,
}
}
#[cfg(feature = "embed-api")]
#[allow(clippy::field_reassign_with_default)]
pub fn with_embedding_config(mut self, embedding: &crate::config::EmbeddingConfig) -> Self {
use crate::proxy::embedder_config::EmbedderConfig;
let mut ec = EmbedderConfig::default();
ec.provider = embedding.provider().to_string();
ec.api_key = embedding.api_key().map(|s| s.to_string());
ec.base_url = embedding.base_url().map(|s| s.to_string());
if let Some(bs) = embedding.batch_size {
ec.max_batch_size = bs;
}
self.warmup_on_start = ec.warmup_on_start;
self.smart_embedder = Some(Arc::new(SmartEmbedder::new(ec)));
self
}
#[cfg(feature = "embed-api")]
pub fn with_semantic_cache(
mut self,
cache_cfg: &crate::config::SemanticCacheSettingsConfig,
) -> Self {
if !cache_cfg.enabled() {
return self;
}
let semantic = Arc::new(SemanticCache::new(
cache_cfg.similarity_threshold(),
cache_cfg.max_entries(),
));
self.semantic_cache = Some(semantic);
self
}
pub fn with_resolved_contexts(self, resolved: &[crate::config::ResolvedContext]) -> Self {
if !resolved.is_empty() {
self.context_manager.install_resolved(resolved);
}
self
}
pub fn with_reload_source(mut self, path: Option<std::path::PathBuf>) -> Self {
self.reload_source = path;
self
}
pub(crate) fn to_app_state(&self, cancel: CancellationToken) -> AppState {
let refresh_worker = self.upstream.as_ref().map(|upstream| {
Arc::new(QueryTrackingRefreshWorker::new(
self.cache.clone(),
upstream.clone(),
self.upstream_id.clone(),
self.refresh_interval,
cancel,
))
});
let query_stats = Arc::new(QueryStatsTracker::new(10000));
let batch_processor = Arc::new(BatchProcessor::new(BatchConfig::default()));
let global_concurrency = Arc::new(tokio::sync::Semaphore::new(self.max_global_connections));
AppState {
cache: self.cache.clone(),
upstream: Arc::new(ArcSwapOption::new(self.upstream.clone())),
upstream_pool: Arc::new(ArcSwapOption::new(self.upstream_pool.clone())),
coalescer: self.coalescer.clone(),
refresh_worker,
scope_filter: self.scope_filter.clone(),
metrics: self.metrics.clone(),
circuit_breaker: self.circuit_breaker.clone(),
audit_log: self.audit_log.clone(),
retry_policy: Arc::new(self.retry_policy.clone()),
adaptive_timeout: self.adaptive_timeout.clone(),
query_stats,
batch_processor,
federated_search: Arc::new(ArcSwap::new(self.federated_search.clone())),
request_queue: self.request_queue.clone(),
upstream_id: self.upstream_id.clone(),
start_time: self.start_time,
context_manager: self.context_manager.clone(),
#[cfg(feature = "embed-api")]
smart_embedder: self.smart_embedder.clone(),
#[cfg(feature = "embed-api")]
semantic_cache: self.semantic_cache.clone(),
degradation_level: Arc::new(std::sync::atomic::AtomicU8::new(0)),
paused: Arc::new(std::sync::atomic::AtomicBool::new(false)),
client_tracker: Arc::new(ClientTracker::new()),
cascade_executor: Arc::new(ArcSwapOption::new(self.cascade_executor.clone())),
agent_registry: Arc::new(ArcSwapOption::new(self.agent_registry.clone())),
cdc_manager: self.cdc_manager.clone(),
global_concurrency,
reload_source: self.reload_source.clone(),
peer_manager: None,
tokio_handle: tokio::runtime::Handle::try_current().ok(),
}
}
pub async fn run(
self,
listen_addr: &str,
http_listen_addr: &str,
cancel: CancellationToken,
) -> anyhow::Result<()> {
let peer_cfg = self.peer_config.clone();
let cache_for_peer = self.cache.clone();
let cdc_for_peer = self.cdc_manager.clone();
let mut state = self.to_app_state(cancel.clone());
state.tokio_handle = Some(tokio::runtime::Handle::current());
if let (Some(ref cdc), Some(ref peer_cfg)) = (&cdc_for_peer, &peer_cfg) {
state.peer_manager = Some(Arc::new(PeerManager::new(
peer_cfg.clone(),
cache_for_peer,
cdc.event_sender(),
cancel.clone(),
)));
}
let initial_level = determine_degradation_level(&state);
state
.degradation_level
.store(initial_level as u8, std::sync::atomic::Ordering::Relaxed);
let auth_config = {
let base = match &self.api_key {
Some(key) => AuthConfig::with_key(key.clone()),
None => AuthConfig::disabled(),
};
let base = match &self.agent_registry {
Some(registry) => base.with_registry(registry.clone()),
None => base,
};
if self.web_ui_enabled {
base.with_web_ui()
} else {
base
}
};
let rate_limit_config = match &self.rate_limiter {
Some(limiter) => RateLimitConfig::with_limiter(limiter.clone()),
None => RateLimitConfig::disabled(),
};
use crate::proxy::grpc::proto::admin_service_server::AdminServiceServer;
use crate::proxy::grpc::proto::context_service_server::ContextServiceServer;
use crate::proxy::grpc::proto::observability_service_server::ObservabilityServiceServer;
use crate::proxy::grpc::proto::search_service_server::SearchServiceServer;
let search_svc = crate::proxy::grpc::search::SearchServiceImpl::new(state.clone());
let admin_svc = crate::proxy::grpc::admin::AdminServiceImpl::new(state.clone());
let context_svc = crate::proxy::grpc::context::ContextServiceImpl::new(state.clone());
let obs_svc =
crate::proxy::grpc::observability::ObservabilityServiceImpl::new(state.clone());
let grpc_addr: SocketAddr = listen_addr.parse()?;
use axum::middleware as axum_middleware;
let protected = Router::new()
.route("/query", post(query::handle_query))
.route("/batch", post(batch::handle_batch))
.route("/federated", post(batch::handle_federated))
.route("/stats", get(status::handle_stats))
.route("/stats/queries", get(status::handle_query_stats))
.route("/metrics", get(status::handle_metrics))
.route("/audit", get(status::handle_audit))
.route("/circuit", get(status::handle_circuit_status))
.route("/queue", get(status::handle_queue_status))
.route("/clients", get(status::handle_clients))
.route("/cache/clear", post(cache::handle_cache_clear))
.route("/cache/warmup", post(cache::handle_cache_warmup))
.route("/cache/evict", post(cache::handle_cache_evict))
.route("/cache/integrity", get(cache::handle_cache_integrity))
.route("/cache/upstreams", get(cache::handle_cache_upstreams))
.route("/cache/entries", get(cache::handle_cache_entries))
.route("/admin/reload", post(admin::handle_admin_reload))
.route("/admin/pause", post(admin::handle_admin_pause))
.route("/admin/resume", post(admin::handle_admin_resume))
.route(
"/admin/metrics/reset",
post(admin::handle_admin_metrics_reset),
)
.route("/admin/agents", get(admin::handle_admin_agents_list))
.route("/admin/agents", post(admin::handle_admin_agents_create))
.route(
"/admin/agents/{id}",
delete(admin::handle_admin_agents_delete),
)
.route(
"/admin/agents/{id}/rotate-key",
post(admin::handle_admin_agents_rotate_key),
)
.route("/contexts", get(context::handle_contexts_list))
.route("/contexts/current", get(context::handle_context_current))
.route("/contexts/switch", post(context::handle_context_switch))
.route("/contexts/create", post(context::handle_context_create))
.route("/contexts/{id}/stats", get(context::handle_context_stats));
let protected = protected
.route_layer(axum_middleware::from_fn_with_state(
rate_limit_config,
rate_limit_middleware,
))
.route_layer(axum_middleware::from_fn_with_state(
auth_config,
auth_middleware,
));
let http_app = Router::new()
.route("/health", get(status::handle_health))
.route("/ready", get(status::handle_ready))
.route("/pool", get(status::handle_pool_status))
.route(
"/metrics/prometheus",
get(status::handle_metrics_prometheus),
)
.route("/peer/status", get(handle_peer_status))
.route("/debug/tokio", get(status::handle_tokio_metrics))
.route("/debug/tokio/dump", get(status::handle_tokio_dump))
.merge(protected);
let http_app = if self.web_ui_enabled {
http_app
.route("/dashboard", get(web_ui::handle_dashboard))
.route("/dashboard/{*path}", get(web_ui::handle_dashboard))
} else {
http_app
};
let http_app = http_app
.with_state(state.clone())
.layer(DefaultBodyLimit::max(256 * 1024));
super::socket_tuning::check_fd_limit();
super::socket_tuning::check_sysctl_recommendations();
let http_addr: SocketAddr = http_listen_addr.parse()?;
let http_listener =
super::socket_tuning::create_tuned_listener(http_addr, &self.socket_tuning)?;
#[cfg(all(feature = "linux-sandbox", target_os = "linux"))]
{
if super::sandbox::needs_sandbox() {
let sandbox_config = super::sandbox::SandboxConfig {
user: None,
group: None,
drop_capabilities: true,
allow_net_bind: grpc_addr.port() < 1024 || http_addr.port() < 1024,
};
match super::sandbox::apply_sandbox(&sandbox_config) {
Ok(()) => info!("Sandbox: capabilities dropped"),
Err(e) => warn!(error = %e, "Sandbox: failed to apply, continuing without"),
}
} else {
info!("Sandbox: not needed (no excess capabilities)");
}
}
info!(%grpc_addr, "gRPC server listening");
info!(%http_addr, "HTTP REST server listening");
if let Some(ref pool) = self.upstream_pool {
info!(endpoints = pool.len(), "Upstream pool configured");
for upstream in pool.all() {
info!(
upstream = %upstream.adapter.identifier(),
weight = upstream.weight,
priority = upstream.priority,
"Upstream endpoint"
);
}
} else if let Some(ref upstream) = self.upstream {
info!(url = %upstream.base_url(), "Upstream configured");
} else {
warn!("No upstream configured (cache-only mode)");
}
if self.api_key.is_some() {
info!("Authentication: enabled (API key required)");
}
if let Some(ref registry) = self.agent_registry {
info!(agents = registry.len(), "Multi-tenancy: enabled");
}
if let Some(ref limiter) = self.rate_limiter {
info!(
rate = limiter.refill_rate(),
burst = limiter.capacity(),
"Rate limiting: enabled"
);
}
if let Some(ref worker) = state.refresh_worker {
let worker = worker.clone();
tokio::spawn(async move {
worker.run().await;
});
info!("Background refresh worker started");
}
if let Some(ref pm) = state.peer_manager {
pm.start().await;
info!(
node_id = %pm.node_id(),
peers = self.peer_config
.as_ref()
.map(|c| c.peers().len())
.unwrap_or(0),
"Peer replication: enabled"
);
}
if let Some(ref cdc) = self.cdc_manager {
info!(node_id = %cdc.node_id(), "CDC event stream: enabled");
}
let cancel_grpc = cancel.clone();
let cancel_http = cancel.clone();
use tonic::codec::CompressionEncoding;
use tonic::service::interceptor::InterceptedService;
let grpc_interceptor_config = crate::proxy::grpc::middleware::GrpcInterceptorConfig::new(
self.agent_registry.clone(),
self.rate_limiter.clone(),
self.api_key.clone(),
);
let grpc_interceptor =
crate::proxy::grpc::middleware::make_interceptor(grpc_interceptor_config);
let peer_shared_secret = self
.peer_config
.as_ref()
.and_then(|c| c.resolve_shared_secret().ok().flatten());
let grpc_listener =
super::socket_tuning::create_tuned_listener(grpc_addr, &self.socket_tuning)?;
let grpc_incoming = tokio_stream::wrappers::TcpListenerStream::new(grpc_listener);
let mut grpc_server_builder = tonic::transport::Server::builder()
.tcp_keepalive(Some(std::time::Duration::from_secs(60)))
.tcp_nodelay(true);
let mut grpc_router = grpc_server_builder
.add_service(InterceptedService::new(
SearchServiceServer::new(search_svc)
.accept_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Zstd)
.send_compressed(CompressionEncoding::Gzip)
.send_compressed(CompressionEncoding::Zstd),
grpc_interceptor.clone(),
))
.add_service(InterceptedService::new(
AdminServiceServer::new(admin_svc)
.accept_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Zstd)
.send_compressed(CompressionEncoding::Gzip)
.send_compressed(CompressionEncoding::Zstd),
grpc_interceptor.clone(),
))
.add_service(InterceptedService::new(
ContextServiceServer::new(context_svc)
.accept_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Zstd)
.send_compressed(CompressionEncoding::Gzip)
.send_compressed(CompressionEncoding::Zstd),
grpc_interceptor.clone(),
))
.add_service(InterceptedService::new(
ObservabilityServiceServer::new(obs_svc)
.accept_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Zstd)
.send_compressed(CompressionEncoding::Gzip)
.send_compressed(CompressionEncoding::Zstd),
grpc_interceptor.clone(),
));
if let Some(ref cdc) = self.cdc_manager {
use super::cdc::proto::cdc_service_server::CdcServiceServer;
let cdc_svc = CdcServiceServer::new(cdc.grpc_service())
.accept_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Zstd)
.send_compressed(CompressionEncoding::Gzip)
.send_compressed(CompressionEncoding::Zstd);
grpc_router = if let Some(ref secret) = peer_shared_secret {
let peer_ix =
crate::proxy::grpc::middleware::make_peer_secret_interceptor(secret.clone());
grpc_router.add_service(InterceptedService::new(cdc_svc, peer_ix))
} else {
grpc_router.add_service(InterceptedService::new(cdc_svc, grpc_interceptor.clone()))
};
}
if let Some(ref pm) = state.peer_manager {
use super::cdc::proto::peer_service_server::PeerServiceServer;
let peer_svc = PeerServiceServer::new(pm.grpc_service())
.accept_compressed(CompressionEncoding::Gzip)
.accept_compressed(CompressionEncoding::Zstd)
.send_compressed(CompressionEncoding::Gzip)
.send_compressed(CompressionEncoding::Zstd);
grpc_router = if let Some(ref secret) = peer_shared_secret {
let peer_ix =
crate::proxy::grpc::middleware::make_peer_secret_interceptor(secret.clone());
grpc_router.add_service(InterceptedService::new(peer_svc, peer_ix))
} else {
grpc_router.add_service(InterceptedService::new(peer_svc, grpc_interceptor.clone()))
};
}
let grpc_server = grpc_router.serve_with_incoming_shutdown(grpc_incoming, async move {
cancel_grpc.cancelled().await;
info!("Shutting down gRPC server...");
});
let http_server = axum::serve(http_listener, http_app).with_graceful_shutdown(async move {
cancel_http.cancelled().await;
info!("Shutting down HTTP server...");
});
let shutdown_timeout = self.shutdown_timeout;
#[cfg(feature = "embed-api")]
if let (Some(embedder), true) = (&self.smart_embedder, self.warmup_on_start) {
if let Err(e) = embedder.warmup().await {
warn!(error = %e, "smart_embedder warmup failed; continuing without");
}
}
tokio::select! {
result = grpc_server => {
if let Err(e) = result {
warn!(error = %e, "gRPC server error");
}
}
result = http_server => {
if let Err(e) = result {
warn!(error = %e, "HTTP server error");
}
}
}
info!(
timeout_secs = shutdown_timeout.as_secs(),
"Servers stopped, draining in-flight requests"
);
#[allow(clippy::arithmetic_side_effects)]
let deadline = tokio::time::Instant::now() + shutdown_timeout;
loop {
let active = state.client_tracker.active_count();
if active == 0 {
info!("All in-flight requests completed");
break;
}
if tokio::time::Instant::now() >= deadline {
warn!(
"Shutdown timeout reached with {} active request(s), forcing exit",
active
);
break;
}
debug!(active, "Waiting for in-flight requests to complete");
#[allow(clippy::arithmetic_side_effects)]
let sleep_until = std::cmp::min(
deadline,
tokio::time::Instant::now() + Duration::from_millis(250),
);
tokio::time::sleep_until(sleep_until).await;
}
info!("Shutdown complete");
Ok(())
}
pub fn cache(&self) -> &Arc<CacheStore> {
&self.cache
}
#[cfg(feature = "persistence")]
pub(crate) fn cache_arc_mut(&mut self) -> &mut Arc<CacheStore> {
&mut self.cache
}
pub(crate) fn context_manager(&self) -> &crate::proxy::context::ContextManager {
&self.context_manager
}
pub fn upstream(&self) -> Option<&Arc<GenericRestAdapter>> {
self.upstream.as_ref()
}
pub fn coalescer(&self) -> &Arc<RequestCoalescer> {
&self.coalescer
}
pub fn rate_limiter(&self) -> Option<&Arc<RateLimiter>> {
self.rate_limiter.as_ref()
}
pub fn requires_auth(&self) -> bool {
self.api_key.is_some()
}
pub fn circuit_breaker(&self) -> &Arc<CircuitBreaker> {
&self.circuit_breaker
}
pub fn audit_log(&self) -> &Arc<AuditLog> {
&self.audit_log
}
pub fn retry_policy(&self) -> &RetryPolicy {
&self.retry_policy
}
pub fn adaptive_timeout(&self) -> &Arc<AdaptiveTimeout> {
&self.adaptive_timeout
}
pub fn upstream_pool(&self) -> Option<&Arc<UpstreamPool>> {
self.upstream_pool.as_ref()
}
pub fn has_upstream(&self) -> bool {
self.upstream_pool.is_some() || self.upstream.is_some()
}
pub fn federated_search(&self) -> &Arc<FederatedSearch> {
&self.federated_search
}
pub fn request_queue(&self) -> &Arc<PriorityQueue<QueryRequest>> {
&self.request_queue
}
pub fn agent_registry(&self) -> Option<&Arc<AgentRegistry>> {
self.agent_registry.as_ref()
}
}
async fn handle_peer_status(State(state): State<AppState>) -> impl IntoResponse {
#[derive(Serialize)]
struct PeerStatusResponse {
enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
node_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_entry_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
cdc_subscribers: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
cdc_sequence: Option<u64>,
}
if let Some(ref pm) = state.peer_manager {
let cdc_info = state
.cdc_manager
.as_ref()
.map(|cdc| (cdc.subscriber_count(), cdc.current_sequence()));
Json(PeerStatusResponse {
enabled: true,
node_id: Some(pm.node_id()),
state: Some(pm.state().as_str().to_string()),
cache_entry_count: Some(state.cache.len()),
cdc_subscribers: cdc_info.map(|(s, _)| s),
cdc_sequence: cdc_info.map(|(_, seq)| seq),
})
} else {
Json(PeerStatusResponse {
enabled: false,
node_id: None,
state: None,
cache_entry_count: None,
cdc_subscribers: None,
cdc_sequence: None,
})
}
}
pub(crate) fn dashboard_http_router() -> Router<AppState> {
Router::new()
.route("/health", get(status::handle_health))
.route("/ready", get(status::handle_ready))
.route("/pool", get(status::handle_pool_status))
.route(
"/metrics/prometheus",
get(status::handle_metrics_prometheus),
)
.route("/peer/status", get(handle_peer_status))
.route("/debug/tokio", get(status::handle_tokio_metrics))
.route("/debug/tokio/dump", get(status::handle_tokio_dump))
.route("/stats", get(status::handle_stats))
.route("/stats/queries", get(status::handle_query_stats))
.route("/metrics", get(status::handle_metrics))
.route("/audit", get(status::handle_audit))
.route("/circuit", get(status::handle_circuit_status))
.route("/queue", get(status::handle_queue_status))
.route("/clients", get(status::handle_clients))
.route("/cache/integrity", get(cache::handle_cache_integrity))
.route("/cache/upstreams", get(cache::handle_cache_upstreams))
.route("/cache/entries", get(cache::handle_cache_entries))
.route("/contexts", get(context::handle_contexts_list))
.route("/contexts/current", get(context::handle_context_current))
.route("/contexts/{id}/stats", get(context::handle_context_stats))
.route("/dashboard", get(web_ui::handle_dashboard))
.route("/dashboard/{*path}", get(web_ui::handle_dashboard))
}
#[cfg(test)]
#[path = "tests/mod_tests.rs"]
pub(crate) mod tests;