Skip to main content

litellm_rs/storage/
mod.rs

1//! Storage layer for the Gateway
2//!
3//! This module provides data persistence and caching functionality.
4
5/// Database storage module
6pub mod database;
7/// Per-dependency runtime status (Healthy / Degraded / Disabled / ...)
8pub mod dependency_status;
9/// File storage module
10pub mod files;
11/// Redis cache module
12pub mod redis;
13/// Vector storage module
14pub 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
26/// Returns the default base directory for local gateway state.
27///
28/// Resolution order:
29/// 1. `LITELLM_DATA_DIR` environment variable (if set and non-empty)
30/// 2. `<data_local_dir>/litellm-rs` (platform-specific)
31/// 3. `/tmp/litellm-rs` (ultimate fallback)
32pub 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/// Main storage layer that orchestrates all storage backends
47#[derive(Debug, Clone)]
48pub struct StorageLayer {
49    /// Database connection pool
50    pub database: Arc<database::Database>,
51    /// Redis connection pool
52    pub redis: Arc<redis::RedisPool>,
53    /// File storage backend
54    pub files: Arc<files::FileStorage>,
55    /// Vector database client (optional)
56    /// Note: Using concrete type instead of trait object for now
57    pub vector: Option<Arc<vector::VectorStoreBackend>>,
58    /// Status of the Redis dependency after init.
59    pub redis_status: DependencyStatus,
60    /// Status of the vector DB dependency after init.
61    pub vector_status: DependencyStatus,
62}
63
64impl StorageLayer {
65    /// Create a new storage layer.
66    ///
67    /// For each optional dependency:
68    /// * if `enabled` is `false` (or the section is absent), the dependency is
69    ///   treated as `Disabled` and a no-op fallback is used,
70    /// * if `enabled` is `true` and init succeeds, status is `Healthy`,
71    /// * if `enabled` is `true`, init fails, and `allow_degraded` is `false`,
72    ///   the call returns `Err` so startup fails loudly,
73    /// * if `enabled` is `true`, init fails, and `allow_degraded` is `true`,
74    ///   the error is logged at `error!` level and the dependency status is
75    ///   set to `Degraded` while the gateway keeps running.
76    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        // Initialize database
87        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        // Initialize Redis (fail-fast unless allow_degraded is set)
92        debug!("Creating Redis connection pool");
93        let (redis, redis_status) = if !config.redis.enabled {
94            // Explicitly disabled — no fallback, no error.
95            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        // Initialize file storage
135        debug!("Initializing file storage");
136        let files = Arc::new(files::FileStorage::new(&config.files).await?);
137
138        // Initialize vector database (fail-fast unless allow_degraded is set)
139        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    /// Run database migrations
224    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    /// Health check for all storage backends
232    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        // Check database health
242        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        // Check Redis health
250        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        // Check file storage health
258        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        // Check vector database health (if configured)
266        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; // Not configured, so consider it healthy
275        }
276
277        // Overall health is true if all configured backends are healthy
278        status.overall = status.database && status.redis && status.files && status.vector;
279
280        Ok(status)
281    }
282
283    /// Close all connections
284    pub async fn close(&self) -> Result<()> {
285        info!("Closing storage connections");
286
287        // Database connections will be closed when Arc is dropped
288        // self.database.close().await?;
289
290        // Close Redis connections
291        self.redis.close().await?;
292
293        // Close file storage
294        self.files.close().await?;
295
296        // Close vector database connections
297        if let Some(vector) = &self.vector {
298            vector.close().await?;
299        }
300
301        info!("Storage connections closed");
302        Ok(())
303    }
304
305    /// Get database pool
306    pub fn db(&self) -> &database::Database {
307        &self.database
308    }
309
310    /// Get Redis pool
311    pub fn redis(&self) -> &redis::RedisPool {
312        &self.redis
313    }
314
315    /// Get file storage
316    pub fn files(&self) -> &files::FileStorage {
317        &self.files
318    }
319
320    /// Get vector store (if available)
321    pub fn vector(&self) -> Option<&vector::VectorStoreBackend> {
322        self.vector.as_deref()
323    }
324
325    // Transaction support removed - SeaORM handles transactions differently
326    // Use the database connection directly for transactional operations
327
328    /// Get a Redis connection
329    pub async fn redis_conn(&self) -> Result<redis::RedisConnection> {
330        self.redis.get_connection().await
331    }
332
333    /// Store file and return file ID
334    pub async fn store_file(&self, filename: &str, content: &[u8]) -> Result<String> {
335        self.files.store(filename, content).await
336    }
337
338    /// Retrieve file content
339    pub async fn get_file(&self, file_id: &str) -> Result<Vec<u8>> {
340        self.files.get(file_id).await
341    }
342
343    /// Delete file
344    pub async fn delete_file(&self, file_id: &str) -> Result<()> {
345        self.files.delete(file_id).await
346    }
347
348    /// Store vector embeddings
349    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    /// Search similar vectors
365    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    /// Cache operations
381    pub async fn cache_get(&self, key: &str) -> Result<Option<String>> {
382        self.redis.get(key).await
383    }
384
385    /// Set cache value with optional TTL
386    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    /// Delete cache key
391    pub async fn cache_delete(&self, key: &str) -> Result<()> {
392        self.redis.delete(key).await
393    }
394
395    /// Check if cache key exists
396    pub async fn cache_exists(&self, key: &str) -> Result<bool> {
397        self.redis.exists(key).await
398    }
399
400    /// Batch cache operations
401    pub async fn cache_mget(&self, keys: &[String]) -> Result<Vec<Option<String>>> {
402        self.redis.mget(keys).await
403    }
404
405    /// Set multiple cache values with optional TTL
406    pub async fn cache_mset(&self, pairs: &[(String, String)], ttl: Option<u64>) -> Result<()> {
407        self.redis.mset(pairs, ttl).await
408    }
409
410    /// List operations
411    /// Push value to list
412    pub async fn list_push(&self, key: &str, value: &str) -> Result<()> {
413        self.redis.list_push(key, value).await
414    }
415
416    /// Pop value from list
417    pub async fn list_pop(&self, key: &str) -> Result<Option<String>> {
418        self.redis.list_pop(key).await
419    }
420
421    /// Get list length
422    pub async fn list_length(&self, key: &str) -> Result<usize> {
423        self.redis.list_length(key).await
424    }
425
426    /// Set operations
427    /// Add member to set
428    pub async fn set_add(&self, key: &str, member: &str) -> Result<()> {
429        self.redis.set_add(key, member).await
430    }
431
432    /// Remove member from set
433    pub async fn set_remove(&self, key: &str, member: &str) -> Result<()> {
434        self.redis.set_remove(key, member).await
435    }
436
437    /// Get all members of set
438    pub async fn set_members(&self, key: &str) -> Result<Vec<String>> {
439        self.redis.set_members(key).await
440    }
441
442    /// Hash operations
443    /// Set hash field value
444    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    /// Get hash field value
449    pub async fn hash_get(&self, key: &str, field: &str) -> Result<Option<String>> {
450        self.redis.hash_get(key, field).await
451    }
452
453    /// Delete hash field
454    pub async fn hash_delete(&self, key: &str, field: &str) -> Result<()> {
455        self.redis.hash_delete(key, field).await
456    }
457
458    /// Get all hash fields and values
459    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/Sub operations
467    pub async fn publish(&self, channel: &str, message: &str) -> Result<()> {
468        self.redis.publish(channel, message).await
469    }
470
471    /// Subscribe to Redis channels
472    /// Subscribe to Redis channels for pub/sub messaging
473    pub async fn subscribe(&self, channels: &[String]) -> Result<redis::Subscription> {
474        self.redis.subscribe(channels).await
475    }
476}
477
478/// Storage health status
479#[derive(Debug, Clone, serde::Serialize)]
480pub struct StorageHealthStatus {
481    /// Database health status
482    pub database: bool,
483    /// Redis health status
484    pub redis: bool,
485    /// File storage health status
486    pub files: bool,
487    /// Vector storage health status
488    pub vector: bool,
489    /// Overall health status
490    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        // This test would require actual database connections
529        // For now, we'll just test that the config is properly structured
530        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            // Port 1 reserved/unreachable; connection_timeout=1 keeps the
671            // test fast.
672            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}