secret_store_sdk/
cache.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::Arc;
3
4/// Cache configuration
5#[derive(Debug, Clone)]
6pub struct CacheConfig {
7    /// Whether caching is enabled
8    pub enabled: bool,
9    /// Maximum number of entries in the cache
10    pub max_entries: u64,
11    /// Default TTL for cache entries in seconds
12    pub default_ttl_secs: u64,
13}
14
15impl Default for CacheConfig {
16    fn default() -> Self {
17        Self {
18            enabled: true,
19            max_entries: crate::DEFAULT_CACHE_MAX_ENTRIES,
20            default_ttl_secs: crate::DEFAULT_CACHE_TTL_SECS,
21        }
22    }
23}
24
25/// Cache statistics
26#[derive(Debug, Clone)]
27pub struct CacheStats {
28    inner: Arc<CacheStatsInner>,
29}
30
31#[derive(Debug, Default)]
32struct CacheStatsInner {
33    hits: AtomicU64,
34    misses: AtomicU64,
35    insertions: AtomicU64,
36    evictions: AtomicU64,
37    expirations: AtomicU64,
38}
39
40impl CacheStats {
41    /// Create new cache statistics
42    pub(crate) fn new() -> Self {
43        Self {
44            inner: Arc::new(CacheStatsInner::default()),
45        }
46    }
47
48    /// Get the number of cache hits
49    pub fn hits(&self) -> u64 {
50        self.inner.hits.load(Ordering::Relaxed)
51    }
52
53    /// Get the number of cache misses
54    pub fn misses(&self) -> u64 {
55        self.inner.misses.load(Ordering::Relaxed)
56    }
57
58    /// Get the number of cache insertions
59    pub fn insertions(&self) -> u64 {
60        self.inner.insertions.load(Ordering::Relaxed)
61    }
62
63    /// Get the number of cache evictions
64    pub fn evictions(&self) -> u64 {
65        self.inner.evictions.load(Ordering::Relaxed)
66    }
67
68    /// Get the number of expired entries
69    pub fn expirations(&self) -> u64 {
70        self.inner.expirations.load(Ordering::Relaxed)
71    }
72
73    /// Get the hit rate as a percentage (0.0-100.0)
74    pub fn hit_rate(&self) -> f64 {
75        let hits = self.hits();
76        let total = hits + self.misses();
77        if total == 0 {
78            0.0
79        } else {
80            (hits as f64 / total as f64) * 100.0
81        }
82    }
83
84    /// Reset all statistics to zero
85    pub fn reset(&self) {
86        self.inner.hits.store(0, Ordering::Relaxed);
87        self.inner.misses.store(0, Ordering::Relaxed);
88        self.inner.insertions.store(0, Ordering::Relaxed);
89        self.inner.evictions.store(0, Ordering::Relaxed);
90        self.inner.expirations.store(0, Ordering::Relaxed);
91    }
92
93    // Internal methods for updating stats
94    pub(crate) fn record_hit(&self) {
95        let _ = self.inner.hits.fetch_add(1, Ordering::Relaxed);
96    }
97
98    pub(crate) fn record_miss(&self) {
99        let _ = self.inner.misses.fetch_add(1, Ordering::Relaxed);
100    }
101
102    pub(crate) fn record_insertion(&self) {
103        let _ = self.inner.insertions.fetch_add(1, Ordering::Relaxed);
104    }
105
106    #[allow(dead_code)]
107    pub(crate) fn record_eviction(&self) {
108        let _ = self.inner.evictions.fetch_add(1, Ordering::Relaxed);
109    }
110
111    pub(crate) fn record_expiration(&self) {
112        let _ = self.inner.expirations.fetch_add(1, Ordering::Relaxed);
113    }
114}
115
116/// Cached secret entry
117#[derive(Debug, Clone)]
118pub(crate) struct CachedSecret {
119    pub value: secrecy::SecretString,
120    pub version: i32,
121    pub expires_at: Option<time::OffsetDateTime>,
122    pub metadata: serde_json::Value,
123    pub updated_at: time::OffsetDateTime,
124    pub etag: Option<String>,
125    pub last_modified: Option<String>,
126    pub cache_expires_at: time::OffsetDateTime,
127}
128
129impl CachedSecret {
130    /// Check if the cache entry has expired
131    pub fn is_expired(&self) -> bool {
132        let now = time::OffsetDateTime::now_utc();
133
134        // Check cache expiry
135        if now >= self.cache_expires_at {
136            return true;
137        }
138
139        // Check secret expiry
140        if let Some(expires_at) = self.expires_at {
141            if now >= expires_at {
142                return true;
143            }
144        }
145
146        false
147    }
148
149    /// Convert to a Secret model
150    pub fn into_secret(self, namespace: String, key: String) -> crate::models::Secret {
151        crate::models::Secret {
152            namespace,
153            key,
154            value: self.value,
155            version: self.version,
156            expires_at: self.expires_at,
157            metadata: self.metadata,
158            updated_at: self.updated_at,
159            etag: self.etag,
160            last_modified: self.last_modified,
161            request_id: None, // Cache hits don't have request IDs
162        }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_cache_config_default() {
172        let config = CacheConfig::default();
173        assert!(config.enabled);
174        assert_eq!(config.max_entries, crate::DEFAULT_CACHE_MAX_ENTRIES);
175        assert_eq!(config.default_ttl_secs, crate::DEFAULT_CACHE_TTL_SECS);
176    }
177
178    #[test]
179    fn test_cache_stats() {
180        let stats = CacheStats::new();
181
182        // Initial state
183        assert_eq!(stats.hits(), 0);
184        assert_eq!(stats.misses(), 0);
185        assert_eq!(stats.hit_rate(), 0.0);
186
187        // Record some activity
188        stats.record_hit();
189        stats.record_hit();
190        stats.record_miss();
191
192        assert_eq!(stats.hits(), 2);
193        assert_eq!(stats.misses(), 1);
194        assert_eq!(stats.hit_rate(), 66.66666666666666);
195
196        // Reset
197        stats.reset();
198        assert_eq!(stats.hits(), 0);
199        assert_eq!(stats.misses(), 0);
200    }
201
202    #[test]
203    fn test_cached_secret_expiry() {
204        use time::Duration;
205
206        let now = time::OffsetDateTime::now_utc();
207
208        // Not expired
209        let cached = CachedSecret {
210            value: secrecy::SecretString::new("value".to_string()),
211            version: 1,
212            expires_at: None,
213            metadata: serde_json::Value::Null,
214            updated_at: now,
215            etag: None,
216            last_modified: None,
217            cache_expires_at: now + Duration::minutes(5),
218        };
219        assert!(!cached.is_expired());
220
221        // Cache expired
222        let cached = CachedSecret {
223            value: secrecy::SecretString::new("value".to_string()),
224            version: 1,
225            expires_at: None,
226            metadata: serde_json::Value::Null,
227            updated_at: now,
228            etag: None,
229            last_modified: None,
230            cache_expires_at: now - Duration::minutes(1),
231        };
232        assert!(cached.is_expired());
233
234        // Secret expired
235        let cached = CachedSecret {
236            value: secrecy::SecretString::new("value".to_string()),
237            version: 1,
238            expires_at: Some(now - Duration::minutes(1)),
239            metadata: serde_json::Value::Null,
240            updated_at: now,
241            etag: None,
242            last_modified: None,
243            cache_expires_at: now + Duration::minutes(5),
244        };
245        assert!(cached.is_expired());
246    }
247}