1pub mod database;
7pub mod dependency_status;
9pub mod files;
11pub mod redis;
13pub mod vector;
15
16pub use dependency_status::DependencyStatus;
17
18use crate::config::models::storage::{DatabaseConfig, StorageConfig};
19use crate::utils::error::gateway_error::{GatewayError, Result};
20use std::path::PathBuf;
21use std::sync::Arc;
22use tracing::{debug, error, info, warn};
23
24const BUDGET_LIMIT_SNAPSHOT_MIGRATION: &str = "m20240501_000001_create_budget_limit_snapshots";
25
26pub fn default_data_dir() -> PathBuf {
33 if let Ok(p) = std::env::var("LITELLM_DATA_DIR")
34 && !p.is_empty()
35 {
36 return PathBuf::from(p);
37 }
38 dirs::data_local_dir()
39 .unwrap_or_else(|| PathBuf::from("/tmp"))
40 .join("litellm-rs")
41}
42
43#[cfg(test)]
44pub(crate) static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
45
46#[derive(Debug, Clone)]
48pub struct StorageLayer {
49 pub database: Arc<database::Database>,
51 pub redis: Arc<redis::RedisPool>,
53 pub files: Arc<files::FileStorage>,
55 pub vector: Option<Arc<vector::VectorStoreBackend>>,
58 pub redis_status: DependencyStatus,
60 pub vector_status: DependencyStatus,
62}
63
64impl StorageLayer {
65 pub async fn new(config: &StorageConfig) -> Result<Self> {
77 crate::config::Validate::validate(&config.redis).map_err(|error| {
78 GatewayError::Config(format!("Invalid Redis configuration: {error}"))
79 })?;
80 crate::config::Validate::validate(config).map_err(|error| {
81 GatewayError::Config(format!("Invalid storage configuration: {error}"))
82 })?;
83
84 info!("Initializing storage layer");
85
86 debug!("Connecting to database");
88 let database = Arc::new(database::Database::new(&config.database).await?);
89 Self::initialize_database_schema(&database, &config.database).await?;
90
91 debug!("Creating Redis connection pool");
93 let (redis, redis_status) = if !config.redis.enabled {
94 info!("Redis disabled in config; using no-op pool");
96 (
97 Arc::new(redis::RedisPool::create_noop()),
98 DependencyStatus::Disabled,
99 )
100 } else {
101 match redis::RedisPool::new(&config.redis).await {
102 Ok(pool) => {
103 if pool.is_noop() {
104 info!("Redis pool initialized in no-op mode");
105 } else {
106 info!("Redis connection established");
107 }
108 (Arc::new(pool), DependencyStatus::Healthy)
109 }
110 Err(e) => {
111 if config.redis.allow_degraded {
112 error!(
113 "Redis init failed but allow_degraded=true; continuing without \
114 cache. Error: {}",
115 e
116 );
117 (
118 Arc::new(redis::RedisPool::create_noop()),
119 DependencyStatus::Degraded,
120 )
121 } else {
122 error!(
123 "Redis init failed and allow_degraded=false; failing startup. \
124 Set storage.redis.allow_degraded=true to keep running in no-op \
125 cache mode. Error: {}",
126 e
127 );
128 return Err(e);
129 }
130 }
131 }
132 };
133
134 debug!("Initializing file storage");
136 let files = Arc::new(files::FileStorage::new(&config.files).await?);
137
138 let (vector, vector_status) = if let Some(ref vector_config) = config.vector_db {
140 debug!("Initializing vector database");
141 match vector::VectorStoreBackend::new(vector_config).await {
142 Ok(v) => (Some(Arc::new(v)), DependencyStatus::Healthy),
143 Err(e) => {
144 if vector_config.allow_degraded {
145 error!(
146 "Vector DB init failed but allow_degraded=true; continuing \
147 without vector backend. Error: {}",
148 e
149 );
150 (None, DependencyStatus::Degraded)
151 } else {
152 error!(
153 "Vector DB init failed and allow_degraded=false; failing \
154 startup. Set storage.vector_db.allow_degraded=true to keep \
155 running without a vector backend. Error: {}",
156 e
157 );
158 return Err(e);
159 }
160 }
161 }
162 } else {
163 debug!("Vector database not configured, skipping");
164 (None, DependencyStatus::Disabled)
165 };
166
167 info!("Storage layer initialized successfully");
168
169 Ok(Self {
170 database,
171 redis,
172 files,
173 vector,
174 redis_status,
175 vector_status,
176 })
177 }
178
179 async fn initialize_database_schema(
180 database: &database::Database,
181 config: &DatabaseConfig,
182 ) -> Result<()> {
183 if Self::should_run_startup_migrations(database, config) {
184 info!("Running database migrations during storage startup");
185 database.migrate().await?;
186 return Ok(());
187 }
188
189 info!(
190 "Database startup migrations disabled; verifying configured schema is already present"
191 );
192 let allowed_pending = if config.allow_degraded {
193 &[BUDGET_LIMIT_SNAPSHOT_MIGRATION][..]
194 } else {
195 &[][..]
196 };
197 database
198 .verify_migrations_applied_except(allowed_pending)
199 .await
200 .map_err(|e| {
201 GatewayError::Storage(format!(
202 "Database schema check failed while storage.database.auto_migrate=false. \
203 Run migrations before startup or set storage.database.auto_migrate=true: {}",
204 e
205 ))
206 })?;
207 database.health_check().await.map_err(|e| {
208 GatewayError::Storage(format!(
209 "Database health check failed while storage.database.auto_migrate=false: {}",
210 e
211 ))
212 })?;
213 Ok(())
214 }
215
216 fn should_run_startup_migrations(
217 database: &database::Database,
218 config: &DatabaseConfig,
219 ) -> bool {
220 !config.enabled || config.auto_migrate || database.is_sqlite_fallback()
221 }
222
223 pub async fn migrate(&self) -> Result<()> {
225 info!("Running database migrations");
226 self.database.migrate().await?;
227 info!("Database migrations completed");
228 Ok(())
229 }
230
231 pub async fn health_check(&self) -> Result<StorageHealthStatus> {
233 let mut status = StorageHealthStatus {
234 database: false,
235 redis: false,
236 files: false,
237 vector: false,
238 overall: false,
239 };
240
241 match self.database.health_check().await {
243 Ok(_) => status.database = true,
244 Err(e) => {
245 warn!("Database health check failed: {}", e);
246 }
247 }
248
249 match self.redis.health_check().await {
251 Ok(_) => status.redis = true,
252 Err(e) => {
253 warn!("Redis health check failed: {}", e);
254 }
255 }
256
257 match self.files.health_check().await {
259 Ok(_) => status.files = true,
260 Err(e) => {
261 warn!("File storage health check failed: {}", e);
262 }
263 }
264
265 if let Some(vector) = &self.vector {
267 match vector.health_check().await {
268 Ok(_) => status.vector = true,
269 Err(e) => {
270 warn!("Vector database health check failed: {}", e);
271 }
272 }
273 } else {
274 status.vector = true; }
276
277 status.overall = status.database && status.redis && status.files && status.vector;
279
280 Ok(status)
281 }
282
283 pub async fn close(&self) -> Result<()> {
285 info!("Closing storage connections");
286
287 self.redis.close().await?;
292
293 self.files.close().await?;
295
296 if let Some(vector) = &self.vector {
298 vector.close().await?;
299 }
300
301 info!("Storage connections closed");
302 Ok(())
303 }
304
305 pub fn db(&self) -> &database::Database {
307 &self.database
308 }
309
310 pub fn redis(&self) -> &redis::RedisPool {
312 &self.redis
313 }
314
315 pub fn files(&self) -> &files::FileStorage {
317 &self.files
318 }
319
320 pub fn vector(&self) -> Option<&vector::VectorStoreBackend> {
322 self.vector.as_deref()
323 }
324
325 pub async fn redis_conn(&self) -> Result<redis::RedisConnection> {
330 self.redis.get_connection().await
331 }
332
333 pub async fn store_file(&self, filename: &str, content: &[u8]) -> Result<String> {
335 self.files.store(filename, content).await
336 }
337
338 pub async fn get_file(&self, file_id: &str) -> Result<Vec<u8>> {
340 self.files.get(file_id).await
341 }
342
343 pub async fn delete_file(&self, file_id: &str) -> Result<()> {
345 self.files.delete(file_id).await
346 }
347
348 pub async fn store_embeddings(
350 &self,
351 id: &str,
352 embeddings: &[f32],
353 metadata: Option<serde_json::Value>,
354 ) -> Result<()> {
355 if let Some(vector) = &self.vector {
356 vector.store(id, embeddings, metadata).await
357 } else {
358 Err(GatewayError::Config(
359 "Vector database not configured".to_string(),
360 ))
361 }
362 }
363
364 pub async fn search_similar(
366 &self,
367 query_vector: &[f32],
368 limit: usize,
369 threshold: Option<f32>,
370 ) -> Result<Vec<vector::SearchResult>> {
371 if let Some(vector) = &self.vector {
372 vector.search(query_vector, limit, threshold).await
373 } else {
374 Err(GatewayError::Config(
375 "Vector database not configured".to_string(),
376 ))
377 }
378 }
379
380 pub async fn cache_get(&self, key: &str) -> Result<Option<String>> {
382 self.redis.get(key).await
383 }
384
385 pub async fn cache_set(&self, key: &str, value: &str, ttl: Option<u64>) -> Result<()> {
387 self.redis.set(key, value, ttl).await
388 }
389
390 pub async fn cache_delete(&self, key: &str) -> Result<()> {
392 self.redis.delete(key).await
393 }
394
395 pub async fn cache_exists(&self, key: &str) -> Result<bool> {
397 self.redis.exists(key).await
398 }
399
400 pub async fn cache_mget(&self, keys: &[String]) -> Result<Vec<Option<String>>> {
402 self.redis.mget(keys).await
403 }
404
405 pub async fn cache_mset(&self, pairs: &[(String, String)], ttl: Option<u64>) -> Result<()> {
407 self.redis.mset(pairs, ttl).await
408 }
409
410 pub async fn list_push(&self, key: &str, value: &str) -> Result<()> {
413 self.redis.list_push(key, value).await
414 }
415
416 pub async fn list_pop(&self, key: &str) -> Result<Option<String>> {
418 self.redis.list_pop(key).await
419 }
420
421 pub async fn list_length(&self, key: &str) -> Result<usize> {
423 self.redis.list_length(key).await
424 }
425
426 pub async fn set_add(&self, key: &str, member: &str) -> Result<()> {
429 self.redis.set_add(key, member).await
430 }
431
432 pub async fn set_remove(&self, key: &str, member: &str) -> Result<()> {
434 self.redis.set_remove(key, member).await
435 }
436
437 pub async fn set_members(&self, key: &str) -> Result<Vec<String>> {
439 self.redis.set_members(key).await
440 }
441
442 pub async fn hash_set(&self, key: &str, field: &str, value: &str) -> Result<()> {
445 self.redis.hash_set(key, field, value).await
446 }
447
448 pub async fn hash_get(&self, key: &str, field: &str) -> Result<Option<String>> {
450 self.redis.hash_get(key, field).await
451 }
452
453 pub async fn hash_delete(&self, key: &str, field: &str) -> Result<()> {
455 self.redis.hash_delete(key, field).await
456 }
457
458 pub async fn hash_get_all(
460 &self,
461 key: &str,
462 ) -> Result<std::collections::HashMap<String, String>> {
463 self.redis.hash_get_all(key).await
464 }
465
466 pub async fn publish(&self, channel: &str, message: &str) -> Result<()> {
468 self.redis.publish(channel, message).await
469 }
470
471 pub async fn subscribe(&self, channels: &[String]) -> Result<redis::Subscription> {
474 self.redis.subscribe(channels).await
475 }
476}
477
478#[derive(Debug, Clone, serde::Serialize)]
480pub struct StorageHealthStatus {
481 pub database: bool,
483 pub redis: bool,
485 pub files: bool,
487 pub vector: bool,
489 pub overall: bool,
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use crate::config::models::file_storage::FileStorageConfig;
497 use crate::config::models::storage::{DatabaseConfig, RedisConfig, RequestLedgerConfig};
498 use tempfile::TempDir;
499
500 #[tokio::test]
501 async fn test_storage_layer_creation() {
502 let config = StorageConfig {
503 config_sync_key_env: None,
504 database: DatabaseConfig {
505 url: "postgresql://localhost:5432/test".to_string(),
506 max_connections: 5,
507 connection_timeout: 5,
508 ssl: false,
509 enabled: true,
510 auto_migrate: false,
511 auto_migrate_configured: false,
512 fallback_to_sqlite: false,
513 allow_degraded: false,
514 },
515 redis: RedisConfig {
516 url: "redis://localhost:6379".to_string(),
517 enabled: true,
518 max_connections: 10,
519 connection_timeout: 5,
520 cluster: false,
521 allow_degraded: false,
522 },
523 files: FileStorageConfig::default(),
524 vector_db: None,
525 request_ledger: RequestLedgerConfig::default(),
526 };
527
528 assert_eq!(config.database.url, "postgresql://localhost:5432/test");
531 assert_eq!(config.redis.url, "redis://localhost:6379");
532 }
533
534 #[tokio::test]
535 async fn test_storage_layer_uses_configured_file_storage_path() {
536 let temp_dir = TempDir::new().expect("temp dir should be created");
537 let configured_path = temp_dir.path().join("configured-files");
538 let config = StorageConfig {
539 config_sync_key_env: None,
540 database: DatabaseConfig::default(),
541 redis: RedisConfig::default(),
542 files: FileStorageConfig {
543 storage_type: "local".to_string(),
544 local_path: Some(configured_path.to_string_lossy().to_string()),
545 s3: None,
546 },
547 vector_db: None,
548 request_ledger: RequestLedgerConfig::default(),
549 };
550
551 let storage = StorageLayer::new(&config)
552 .await
553 .expect("storage layer should initialize with configured local file storage");
554 let file_id = storage
555 .store_file("configured.txt", b"configured storage")
556 .await
557 .expect("file should be stored");
558
559 let expected_path = configured_path
560 .join(&file_id[..2.min(file_id.len())])
561 .join(&file_id);
562 assert!(
563 expected_path.exists(),
564 "stored file should use configured path: {}",
565 expected_path.display()
566 );
567 }
568
569 #[tokio::test]
570 async fn default_storage_layer_runs_in_memory_sqlite_migrations() {
571 let config = StorageConfig {
572 config_sync_key_env: None,
573 database: DatabaseConfig::default(),
574 redis: RedisConfig::default(),
575 files: FileStorageConfig::default(),
576 vector_db: None,
577 request_ledger: RequestLedgerConfig::default(),
578 };
579
580 let storage = StorageLayer::new(&config)
581 .await
582 .expect("default storage layer should initialize");
583 let snapshots = storage
584 .database
585 .load_budget_limit_snapshots()
586 .await
587 .expect("default in-memory SQLite should have migrated budget tables");
588
589 assert!(snapshots.is_empty());
590 }
591
592 #[tokio::test]
593 async fn configured_database_without_auto_migrate_requires_existing_schema() {
594 let config = StorageConfig {
595 config_sync_key_env: None,
596 database: DatabaseConfig {
597 url: "sqlite::memory:".to_string(),
598 max_connections: 1,
599 connection_timeout: 1,
600 ssl: false,
601 enabled: true,
602 auto_migrate: false,
603 auto_migrate_configured: false,
604 fallback_to_sqlite: false,
605 allow_degraded: false,
606 },
607 redis: RedisConfig::default(),
608 files: FileStorageConfig::default(),
609 vector_db: None,
610 request_ledger: RequestLedgerConfig::default(),
611 };
612
613 let err = match StorageLayer::new(&config).await {
614 Ok(_) => panic!("configured database without schema must fail when auto_migrate=false"),
615 Err(err) => err,
616 };
617 assert!(
618 err.to_string().contains("auto_migrate=false"),
619 "error should explain how to enable or run migrations, got: {}",
620 err
621 );
622 }
623
624 #[tokio::test]
625 async fn configured_database_with_auto_migrate_runs_startup_migrations() {
626 let config = StorageConfig {
627 config_sync_key_env: None,
628 database: sqlite_db_config(),
629 redis: RedisConfig::default(),
630 files: FileStorageConfig::default(),
631 vector_db: None,
632 request_ledger: RequestLedgerConfig::default(),
633 };
634
635 let storage = match StorageLayer::new(&config).await {
636 Ok(storage) => storage,
637 Err(err) => panic!("auto_migrate=true should initialize SQLite schema: {}", err),
638 };
639 let snapshots = match storage.database.load_budget_limit_snapshots().await {
640 Ok(snapshots) => snapshots,
641 Err(err) => panic!("startup migration should create budget tables: {}", err),
642 };
643
644 assert!(snapshots.is_empty());
645 }
646
647 #[tokio::test]
648 async fn storage_layer_migrate_is_idempotent_after_startup_migration() {
649 let config = StorageConfig {
650 config_sync_key_env: None,
651 database: DatabaseConfig::default(),
652 redis: RedisConfig::default(),
653 files: FileStorageConfig::default(),
654 vector_db: None,
655 request_ledger: RequestLedgerConfig::default(),
656 };
657
658 let storage = StorageLayer::new(&config)
659 .await
660 .expect("default storage layer should initialize");
661
662 storage
663 .migrate()
664 .await
665 .expect("explicit migration after startup migration should be idempotent");
666 }
667
668 fn unreachable_redis_config(allow_degraded: bool) -> RedisConfig {
669 RedisConfig {
670 url: "redis://127.0.0.1:1".to_string(),
673 enabled: true,
674 max_connections: 1,
675 connection_timeout: 1,
676 cluster: false,
677 allow_degraded,
678 }
679 }
680
681 fn sqlite_db_config() -> DatabaseConfig {
682 DatabaseConfig {
683 url: "sqlite::memory:".to_string(),
684 max_connections: 1,
685 connection_timeout: 1,
686 ssl: false,
687 enabled: true,
688 auto_migrate: true,
689 auto_migrate_configured: false,
690 fallback_to_sqlite: false,
691 allow_degraded: false,
692 }
693 }
694
695 #[tokio::test]
696 async fn redis_enabled_failing_without_allow_degraded_fails_startup() {
697 let config = StorageConfig {
698 config_sync_key_env: None,
699 database: sqlite_db_config(),
700 redis: unreachable_redis_config(false),
701 files: FileStorageConfig::default(),
702 vector_db: None,
703 request_ledger: RequestLedgerConfig::default(),
704 };
705
706 let result = StorageLayer::new(&config).await;
707 assert!(
708 result.is_err(),
709 "enabled redis that cannot connect must fail startup when allow_degraded=false"
710 );
711 }
712
713 #[tokio::test]
714 async fn redis_enabled_failing_with_allow_degraded_continues_with_noop() {
715 let config = StorageConfig {
716 config_sync_key_env: None,
717 database: sqlite_db_config(),
718 redis: unreachable_redis_config(true),
719 files: FileStorageConfig::default(),
720 vector_db: None,
721 request_ledger: RequestLedgerConfig::default(),
722 };
723
724 let storage = StorageLayer::new(&config)
725 .await
726 .expect("allow_degraded=true should allow startup with no-op redis pool");
727 assert!(
728 storage.redis.is_noop(),
729 "degraded redis must fall back to a no-op pool"
730 );
731 assert_eq!(storage.redis_status, DependencyStatus::Degraded);
732 }
733
734 #[tokio::test]
735 async fn redis_disabled_uses_noop_without_error() {
736 let redis = RedisConfig {
737 enabled: false,
738 ..RedisConfig::default()
739 };
740 let config = StorageConfig {
741 config_sync_key_env: None,
742 database: sqlite_db_config(),
743 redis,
744 files: FileStorageConfig::default(),
745 vector_db: None,
746 request_ledger: RequestLedgerConfig::default(),
747 };
748
749 let storage = StorageLayer::new(&config)
750 .await
751 .expect("disabled redis must always succeed");
752 assert!(storage.redis.is_noop());
753 assert_eq!(storage.redis_status, DependencyStatus::Disabled);
754 }
755
756 #[cfg(feature = "sqlite")]
757 include!("vector_startup_tests.rs");
758}