secret_store_sdk/
cache.rs1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::Arc;
3
4#[derive(Debug, Clone)]
6pub struct CacheConfig {
7 pub enabled: bool,
9 pub max_entries: u64,
11 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#[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 pub(crate) fn new() -> Self {
43 Self {
44 inner: Arc::new(CacheStatsInner::default()),
45 }
46 }
47
48 pub fn hits(&self) -> u64 {
50 self.inner.hits.load(Ordering::Relaxed)
51 }
52
53 pub fn misses(&self) -> u64 {
55 self.inner.misses.load(Ordering::Relaxed)
56 }
57
58 pub fn insertions(&self) -> u64 {
60 self.inner.insertions.load(Ordering::Relaxed)
61 }
62
63 pub fn evictions(&self) -> u64 {
65 self.inner.evictions.load(Ordering::Relaxed)
66 }
67
68 pub fn expirations(&self) -> u64 {
70 self.inner.expirations.load(Ordering::Relaxed)
71 }
72
73 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 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 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#[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 pub fn is_expired(&self) -> bool {
132 let now = time::OffsetDateTime::now_utc();
133
134 if now >= self.cache_expires_at {
136 return true;
137 }
138
139 if let Some(expires_at) = self.expires_at {
141 if now >= expires_at {
142 return true;
143 }
144 }
145
146 false
147 }
148
149 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, }
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 assert_eq!(stats.hits(), 0);
184 assert_eq!(stats.misses(), 0);
185 assert_eq!(stats.hit_rate(), 0.0);
186
187 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 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 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 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 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}