use anyhow::Result;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::signal;
use tower::ServiceBuilder;
use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer};
use tower_http::timeout::TimeoutLayer;
use tracing::{error, info};
use crate::{
auth,
config::ServerConfig,
embeddings::minilm::pre_init_ort_runtime,
handlers::{self, AppState, MultiUserMemoryManager},
metrics, middleware,
};
#[cfg(feature = "telemetry")]
use crate::tracing_setup;
use crate::constants::{
DATABASE_FLUSH_TIMEOUT_SECS, GRACEFUL_SHUTDOWN_TIMEOUT_SECS, VECTOR_INDEX_SAVE_TIMEOUT_SECS,
};
const SERVER_DRAIN_TIMEOUT_SECS: u64 = 5;
pub struct ServerRunConfig {
pub host: String,
pub port: u16,
pub storage_path: PathBuf,
pub production: bool,
pub rate_limit: u64,
pub max_concurrent: usize,
}
pub fn run(config: ServerRunConfig) -> Result<()> {
unsafe {
std::env::set_var("SHODH_HOST", &config.host);
std::env::set_var("SHODH_PORT", config.port.to_string());
std::env::set_var(
"SHODH_MEMORY_PATH",
config.storage_path.to_string_lossy().to_string(),
);
if config.production {
std::env::set_var("SHODH_ENV", "production");
}
std::env::set_var("SHODH_RATE_LIMIT", config.rate_limit.to_string());
std::env::set_var("SHODH_MAX_CONCURRENT", config.max_concurrent.to_string());
}
pre_init_ort_runtime(false);
if std::env::var("RUST_LOG").is_err() {
unsafe {
std::env::set_var("RUST_LOG", "shodh_memory=info,tower_http=warn");
}
}
let _ = dotenvy::dotenv();
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed to build tokio runtime")
.block_on(async_main())
}
async fn async_main() -> Result<()> {
#[cfg(feature = "telemetry")]
{
tracing_setup::init_tracing().expect("Failed to initialize tracing");
}
#[cfg(not(feature = "telemetry"))]
{
tracing_subscriber::fmt::init();
}
print_banner();
auth::log_security_status();
metrics::register_metrics().expect("Failed to register metrics");
let server_config = ServerConfig::from_env();
print_config(&server_config);
let manager: AppState = Arc::new(MultiUserMemoryManager::new(
server_config.storage_path.clone(),
server_config.clone(),
)?);
print_storage_stats(&server_config.storage_path);
let manager_for_shutdown = Arc::clone(&manager);
start_maintenance_scheduler(
Arc::clone(&manager),
server_config.maintenance_interval_secs,
);
start_reminder_scheduler(Arc::clone(&manager));
if server_config.backup_enabled && server_config.backup_interval_secs > 0 {
start_backup_scheduler(
Arc::clone(&manager),
server_config.backup_interval_secs,
server_config.backup_max_count,
);
}
#[cfg(feature = "zenoh")]
let zenoh_handle = {
let zenoh_config = crate::zenoh_transport::ZenohConfig::from_env();
if zenoh_config.enabled {
match crate::zenoh_transport::start(Arc::clone(&manager), zenoh_config).await {
Ok(handle) => {
info!("Zenoh transport started successfully");
Some(handle)
}
Err(e) => {
error!("Failed to start Zenoh transport: {}. HTTP server will continue without Zenoh.", e);
None
}
}
} else {
info!("Zenoh transport: disabled (set SHODH_ZENOH_ENABLED=true to enable)");
None
}
};
let rate_limit_enabled = server_config.rate_limit_per_second > 0;
let governor_layer = if rate_limit_enabled {
let rps = server_config.rate_limit_per_second.max(1);
let cell_interval = std::time::Duration::from_nanos(1_000_000_000 / rps);
let governor_conf = GovernorConfigBuilder::default()
.period(cell_interval)
.burst_size(server_config.rate_limit_burst)
.finish()
.expect("Failed to build governor rate limiter configuration");
info!(
"Rate limiting: {} req/sec (cell interval: {:?}), burst of {}",
server_config.rate_limit_per_second, cell_interval, server_config.rate_limit_burst
);
Some(GovernorLayer::new(governor_conf))
} else {
info!("Rate limiting: disabled (SHODH_RATE_LIMIT=0)");
None
};
let cors = server_config.cors.to_layer();
let public_routes = handlers::build_public_routes(Arc::clone(&manager)).route(
"/",
axum::routing::get(|| async {
axum::Json(serde_json::json!({
"name": "shodh-memory",
"version": env!("CARGO_PKG_VERSION"),
"description": "Cognitive Memory for AI Agents",
"health": "/health",
"api": {
"remember": "POST /api/remember",
"recall": "POST /api/recall",
"forget": "POST /api/forget",
"todos": "GET /api/todos",
"graph": "GET /api/graph/stats"
},
"docs": "https://github.com/varun29ankuS/shodh-memory"
}))
}),
);
let protected_routes = if let Some(governor) = governor_layer {
handlers::build_protected_routes(Arc::clone(&manager))
.layer(axum::middleware::from_fn(auth::auth_middleware))
.layer(governor)
} else {
handlers::build_protected_routes(Arc::clone(&manager))
.layer(axum::middleware::from_fn(auth::auth_middleware))
};
let request_timeout = std::time::Duration::from_secs(server_config.request_timeout_secs);
let app = axum::Router::new()
.merge(public_routes)
.merge(protected_routes)
.layer(
ServiceBuilder::new()
.layer(axum::middleware::from_fn(middleware::security_headers))
.layer(axum::middleware::from_fn(middleware::track_metrics))
.layer(TimeoutLayer::with_status_code(
axum::http::StatusCode::REQUEST_TIMEOUT,
request_timeout,
))
.layer(tower::limit::ConcurrencyLimitLayer::new(
server_config.max_concurrent_requests,
))
.layer(cors),
);
#[cfg(feature = "telemetry")]
let app = app.layer(axum::middleware::from_fn(
tracing_setup::trace_propagation::propagate_trace_context,
));
let host = &server_config.host;
let port = server_config.port;
let addr: SocketAddr = format!("{}:{}", host, port).parse().unwrap_or_else(|_| {
tracing::warn!("Invalid SHODH_HOST '{}', falling back to 127.0.0.1", host);
SocketAddr::from(([127, 0, 0, 1], port))
});
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
print_ready_message(addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
let shutdown_notify = Arc::new(tokio::sync::Notify::new());
let shutdown_listener = shutdown_notify.clone();
let server = axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(async move {
shutdown_listener.notified().await;
});
let mut server_handle = tokio::spawn(async move { server.await });
shutdown_signal_with_drain().await;
shutdown_notify.notify_one();
info!(
"Waiting up to {}s for in-flight requests...",
SERVER_DRAIN_TIMEOUT_SECS
);
match tokio::time::timeout(
std::time::Duration::from_secs(SERVER_DRAIN_TIMEOUT_SECS),
&mut server_handle,
)
.await
{
Ok(Ok(Ok(()))) => info!("Server stopped gracefully"),
Ok(Ok(Err(e))) => error!("Server error: {}", e),
Ok(Err(e)) => error!("Server task panicked: {}", e),
Err(_) => {
info!(
"Server drain timed out after {}s, aborting server task",
SERVER_DRAIN_TIMEOUT_SECS
);
server_handle.abort();
}
}
manager_for_shutdown.task_tracker.close();
let task_timeout = std::time::Duration::from_secs(10);
info!(
"Waiting up to {}s for background graph tasks...",
task_timeout.as_secs()
);
if tokio::time::timeout(task_timeout, manager_for_shutdown.task_tracker.wait())
.await
.is_err()
{
tracing::warn!(
"Background task drain timed out after {}s, proceeding with shutdown",
task_timeout.as_secs()
);
} else {
info!("All background graph tasks completed");
}
#[cfg(feature = "zenoh")]
if let Some(handle) = zenoh_handle {
handle.shutdown().await;
}
run_shutdown_cleanup(manager_for_shutdown).await;
Ok(())
}
fn start_maintenance_scheduler(manager: AppState, interval_secs: u64) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_secs));
interval.tick().await;
loop {
interval.tick().await;
let extractor = manager.streaming_extractor().clone();
let cleaned = extractor.cleanup_stale_sessions().await;
if cleaned > 0 {
tracing::debug!("Cleaned {} stale streaming sessions", cleaned);
}
let session_cleaned = manager.session_store().cleanup_stale_sessions();
if session_cleaned > 0 {
tracing::debug!("Ended {} stale user sessions", session_cleaned);
}
{
let tracker = &manager.habituation_tracker;
let cutoff = chrono::Utc::now() - chrono::Duration::hours(24);
let mut pruned = 0usize;
tracker.retain(|_user_id, inner| {
inner.retain(|_mem_id, entry| {
let keep = entry.last_surfaced > cutoff;
if !keep {
pruned += 1;
}
keep
});
!inner.is_empty()
});
if pruned > 0 {
tracing::debug!("Pruned {} stale habituation entries (>24h)", pruned);
}
}
let manager_clone = Arc::clone(&manager);
tokio::task::spawn_blocking(move || {
manager_clone.run_maintenance_all_users();
});
}
});
info!(
"Background maintenance scheduler started (interval: {}s)",
interval_secs
);
}
fn start_backup_scheduler(manager: AppState, interval_secs: u64, max_backups: usize) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_secs));
interval.tick().await;
loop {
interval.tick().await;
info!("Starting scheduled backup run...");
let manager_clone = Arc::clone(&manager);
let backed_up = tokio::task::spawn_blocking(move || {
manager_clone.run_backup_all_users(max_backups)
})
.await
.unwrap_or(0);
if backed_up > 0 {
info!("Scheduled backup completed: {} users backed up", backed_up);
}
}
});
info!(
"Automatic backup scheduler started (interval: {}h, keep: {} backups)",
interval_secs / 3600,
max_backups
);
}
fn start_reminder_scheduler(manager: AppState) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
interval.tick().await;
loop {
interval.tick().await;
let manager_clone = Arc::clone(&manager);
match tokio::task::spawn_blocking(move || manager_clone.check_and_emit_due_reminders())
.await
{
Ok(triggered) => {
if triggered > 0 {
info!("Active reminder check: {} reminder(s) triggered", triggered);
}
}
Err(e) => {
error!("Reminder scheduler task panicked: {}", e);
}
}
}
});
info!("Active reminder scheduler started (interval: 60s)");
}
async fn shutdown_signal_with_drain() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
info!("Shutdown signal received, starting graceful shutdown");
}
async fn run_shutdown_cleanup(manager: AppState) {
info!("Proceeding with database flush...");
let cleanup_future = async {
let manager_for_flush = Arc::clone(&manager);
let flush_handle =
tokio::task::spawn_blocking(move || manager_for_flush.flush_all_databases());
match tokio::time::timeout(
std::time::Duration::from_secs(DATABASE_FLUSH_TIMEOUT_SECS),
flush_handle,
)
.await
{
Ok(Ok(Ok(()))) => info!("Databases flushed successfully"),
Ok(Ok(Err(e))) => error!("Failed to flush databases: {}", e),
Ok(Err(e)) => error!("Flush task panicked: {}", e),
Err(_) => error!(
"Database flush timed out after {}s",
DATABASE_FLUSH_TIMEOUT_SECS
),
}
info!("Persisting vector indices...");
let manager_for_save = Arc::clone(&manager);
let save_handle =
tokio::task::spawn_blocking(move || manager_for_save.save_all_vector_indices());
match tokio::time::timeout(
std::time::Duration::from_secs(VECTOR_INDEX_SAVE_TIMEOUT_SECS),
save_handle,
)
.await
{
Ok(Ok(Ok(()))) => info!("Vector indices saved successfully"),
Ok(Ok(Err(e))) => error!("Failed to save vector indices: {}", e),
Ok(Err(e)) => error!("Save task panicked: {}", e),
Err(_) => error!(
"Vector index save timed out after {}s",
VECTOR_INDEX_SAVE_TIMEOUT_SECS
),
}
#[cfg(feature = "telemetry")]
tracing_setup::shutdown_tracing();
};
match tokio::time::timeout(
std::time::Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS),
cleanup_future,
)
.await
{
Ok(()) => info!("Server shutdown complete"),
Err(_) => {
error!(
"Graceful shutdown timed out after {}s, forcing exit",
GRACEFUL_SHUTDOWN_TIMEOUT_SECS
);
std::process::exit(1);
}
}
}
fn print_banner() {
eprintln!();
eprintln!(" ╔═══════════════════════════════════════════════════╗");
eprintln!(
" ║ 🧠 Shodh-Memory Server v{} ║",
env!("CARGO_PKG_VERSION")
);
eprintln!(" ║ Cognitive Memory for AI Agents ║");
eprintln!(" ╚═══════════════════════════════════════════════════╝");
eprintln!();
}
fn print_config(config: &ServerConfig) {
eprintln!(" Configuration:");
eprintln!(
" Mode: {}",
if config.is_production {
"PRODUCTION"
} else {
"Development"
}
);
eprintln!(" Host: {}", config.host);
eprintln!(" Port: {}", config.port);
eprintln!(" Storage: {}", config.storage_path.display());
eprintln!();
}
fn print_storage_stats(storage_path: &std::path::Path) {
if storage_path.exists() {
let disk_usage = calculate_dir_size(storage_path);
let user_count = count_user_directories(storage_path);
eprintln!(" 💾 Storage Statistics:");
eprintln!(
" Location: {}",
storage_path
.canonicalize()
.unwrap_or_else(|_| storage_path.to_path_buf())
.display()
);
eprintln!(" Disk used: {}", format_bytes(disk_usage));
eprintln!(" Users: {}", user_count);
eprintln!();
} else {
eprintln!(" 💾 Storage: New database (no existing data)");
eprintln!();
}
}
fn print_ready_message(addr: SocketAddr) {
use std::io::Write;
let _ = std::io::stderr().flush();
eprintln!();
eprintln!(" 🚀 Server ready!");
eprintln!(" API: http://{}", addr);
eprintln!(" Health: http://{}/health", addr);
eprintln!(" Stream: ws://{}/api/stream", addr);
#[cfg(feature = "zenoh")]
{
let zenoh_enabled = std::env::var("SHODH_ZENOH_ENABLED")
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
if zenoh_enabled {
let prefix =
std::env::var("SHODH_ZENOH_PREFIX").unwrap_or_else(|_| "shodh".to_string());
eprintln!(
" Zenoh: {}/*/{{remember,recall,forget,stream,mission}}",
prefix
);
eprintln!(" Fleet: {}/fleet/*", prefix);
}
}
eprintln!();
eprintln!(" Press Ctrl+C to stop");
eprintln!();
let _ = std::io::stderr().flush();
}
fn calculate_dir_size(path: &std::path::Path) -> u64 {
let mut total = 0;
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
total += calculate_dir_size(&path);
} else if let Ok(metadata) = entry.metadata() {
total += metadata.len();
}
}
}
total
}
fn count_user_directories(path: &std::path::Path) -> usize {
std::fs::read_dir(path)
.map(|entries| {
entries
.flatten()
.filter(|e| {
let name = e.file_name();
let name_str = name.to_string_lossy();
e.path().is_dir()
&& name_str != "audit_logs"
&& name_str != "backups"
&& name_str != "feedback"
&& name_str != "semantic_facts"
&& name_str != "files"
&& name_str != "prospective"
&& name_str != "todos"
})
.count()
})
.unwrap_or(0)
}
fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} bytes", bytes)
}
}