Skip to main content

mockforge_registry_server/
cache.rs

1//! Redis caching utilities for frequently accessed data
2//!
3//! Provides caching layer for organization data, user data, settings, and marketplace content
4//! to reduce database load and improve response times.
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use crate::redis::RedisPool;
11
12/// Cache key prefixes for different data types
13pub mod keys {
14    pub const ORG: &str = "cache:org:";
15    pub const USER: &str = "cache:user:";
16    pub const ORG_SETTING: &str = "cache:org_setting:";
17    pub const USER_SETTING: &str = "cache:user_setting:";
18    pub const SUBSCRIPTION: &str = "cache:subscription:";
19    pub const PLUGIN: &str = "cache:plugin:";
20    pub const TEMPLATE: &str = "cache:template:";
21    pub const SCENARIO: &str = "cache:scenario:";
22    pub const ORG_MEMBERS: &str = "cache:org_members:";
23}
24
25/// Cache TTL constants (in seconds)
26pub mod ttl {
27
28    /// Short-lived cache (1 minute) - for frequently changing data
29    pub const SHORT: u64 = 60;
30
31    /// Medium cache (5 minutes) - for moderately changing data
32    pub const MEDIUM: u64 = 300;
33
34    /// Long cache (15 minutes) - for relatively static data
35    pub const LONG: u64 = 900;
36
37    /// Very long cache (1 hour) - for static data
38    pub const VERY_LONG: u64 = 3600;
39
40    /// Organization data cache (5 minutes)
41    pub const ORG: u64 = MEDIUM;
42
43    /// User data cache (5 minutes)
44    pub const USER: u64 = MEDIUM;
45
46    /// Settings cache (15 minutes)
47    pub const SETTINGS: u64 = LONG;
48
49    /// Subscription cache (5 minutes)
50    pub const SUBSCRIPTION: u64 = MEDIUM;
51
52    /// Marketplace content cache (15 minutes)
53    pub const MARKETPLACE: u64 = LONG;
54
55    /// Org members cache (5 minutes)
56    pub const ORG_MEMBERS: u64 = MEDIUM;
57}
58
59/// Cache wrapper for Redis operations
60pub struct Cache {
61    redis: RedisPool,
62}
63
64impl Cache {
65    /// Create a new cache instance
66    pub fn new(redis: RedisPool) -> Self {
67        Self { redis }
68    }
69
70    /// Get a cached value as JSON
71    pub async fn get<T>(&self, key: &str) -> Result<Option<T>>
72    where
73        T: for<'de> Deserialize<'de>,
74    {
75        match self.redis.get(key).await? {
76            Some(value) => {
77                let deserialized: T = serde_json::from_str(&value)?;
78                Ok(Some(deserialized))
79            }
80            None => Ok(None),
81        }
82    }
83
84    /// Set a cached value as JSON
85    pub async fn set<T>(&self, key: &str, value: &T, ttl: u64) -> Result<()>
86    where
87        T: Serialize,
88    {
89        let serialized = serde_json::to_string(value)?;
90        self.redis.set_with_expiry(key, &serialized, ttl).await?;
91        Ok(())
92    }
93
94    /// Delete a cached value
95    pub async fn delete(&self, key: &str) -> Result<()> {
96        self.redis.delete(key).await?;
97        Ok(())
98    }
99
100    /// Delete multiple keys matching a glob pattern using Redis SCAN
101    pub async fn delete_pattern(&self, pattern: &str) -> Result<()> {
102        let keys = self.redis.scan_keys(pattern).await?;
103        for key in keys {
104            if let Err(e) = self.redis.delete(&key).await {
105                tracing::warn!("Failed to delete cache key {}: {}", key, e);
106            }
107        }
108        Ok(())
109    }
110
111    /// Invalidate organization-related caches
112    pub async fn invalidate_org(&self, org_id: &Uuid) -> Result<()> {
113        if let Err(e) = self.delete(&format!("{}:{}", keys::ORG, org_id)).await {
114            tracing::warn!("Failed to invalidate org cache for {}: {}", org_id, e);
115        }
116        if let Err(e) = self.delete(&format!("{}:{}", keys::ORG_MEMBERS, org_id)).await {
117            tracing::warn!("Failed to invalidate org members cache for {}: {}", org_id, e);
118        }
119        if let Err(e) = self.delete_pattern(&format!("{}:{}:*", keys::ORG_SETTING, org_id)).await {
120            tracing::warn!("Failed to invalidate org settings cache for {}: {}", org_id, e);
121        }
122        Ok(())
123    }
124
125    /// Invalidate user-related caches
126    pub async fn invalidate_user(&self, user_id: &Uuid) -> Result<()> {
127        if let Err(e) = self.delete(&format!("{}:{}", keys::USER, user_id)).await {
128            tracing::warn!("Failed to invalidate user cache for {}: {}", user_id, e);
129        }
130        if let Err(e) = self.delete_pattern(&format!("{}:{}:*", keys::USER_SETTING, user_id)).await
131        {
132            tracing::warn!("Failed to invalidate user settings cache for {}: {}", user_id, e);
133        }
134        Ok(())
135    }
136
137    /// Invalidate subscription cache
138    pub async fn invalidate_subscription(&self, org_id: &Uuid) -> Result<()> {
139        let key = format!("{}:{}", keys::SUBSCRIPTION, org_id);
140        self.delete(&key).await?;
141        Ok(())
142    }
143
144    /// Invalidate marketplace content cache
145    pub async fn invalidate_marketplace(
146        &self,
147        content_type: &str,
148        content_id: &Uuid,
149    ) -> Result<()> {
150        let key = match content_type {
151            "plugin" => format!("{}:{}", keys::PLUGIN, content_id),
152            "template" => format!("{}:{}", keys::TEMPLATE, content_id),
153            "scenario" => format!("{}:{}", keys::SCENARIO, content_id),
154            _ => return Ok(()),
155        };
156        self.delete(&key).await?;
157        Ok(())
158    }
159
160    /// Get or set pattern: Try cache first, fallback to database query
161    pub async fn get_or_set<F, Fut, T>(&self, key: &str, ttl: u64, f: F) -> Result<T>
162    where
163        F: FnOnce() -> Fut,
164        Fut: std::future::Future<Output = Result<T>>,
165        T: Serialize + for<'de> Deserialize<'de>,
166    {
167        // Try cache first
168        if let Some(cached) = self.get::<T>(key).await? {
169            return Ok(cached);
170        }
171
172        // Cache miss - fetch from database
173        let value = f().await?;
174
175        // Store in cache (non-blocking - don't fail if cache write fails)
176        if let Err(e) = self.set(key, &value, ttl).await {
177            tracing::warn!("Failed to cache value for key {}: {}", key, e);
178        }
179
180        Ok(value)
181    }
182}
183
184/// Helper function to generate organization cache key
185pub fn org_cache_key(org_id: &Uuid) -> String {
186    format!("{}:{}", keys::ORG, org_id)
187}
188
189/// Helper function to generate user cache key
190pub fn user_cache_key(user_id: &Uuid) -> String {
191    format!("{}:{}", keys::USER, user_id)
192}
193
194/// Helper function to generate org setting cache key
195pub fn org_setting_cache_key(org_id: &Uuid, setting_key: &str) -> String {
196    format!("{}:{}:{}", keys::ORG_SETTING, org_id, setting_key)
197}
198
199/// Helper function to generate user setting cache key
200pub fn user_setting_cache_key(user_id: &Uuid, setting_key: &str) -> String {
201    format!("{}:{}:{}", keys::USER_SETTING, user_id, setting_key)
202}
203
204/// Helper function to generate subscription cache key
205pub fn subscription_cache_key(org_id: &Uuid) -> String {
206    format!("{}:{}", keys::SUBSCRIPTION, org_id)
207}
208
209/// Helper function to generate org members cache key
210pub fn org_members_cache_key(org_id: &Uuid) -> String {
211    format!("{}:{}", keys::ORG_MEMBERS, org_id)
212}
213
214/// Helper function to generate plugin cache key
215pub fn plugin_cache_key(plugin_id: &Uuid) -> String {
216    format!("{}:{}", keys::PLUGIN, plugin_id)
217}
218
219/// Helper function to generate template cache key
220pub fn template_cache_key(template_id: &Uuid) -> String {
221    format!("{}:{}", keys::TEMPLATE, template_id)
222}
223
224/// Helper function to generate scenario cache key
225pub fn scenario_cache_key(scenario_id: &Uuid) -> String {
226    format!("{}:{}", keys::SCENARIO, scenario_id)
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    // Cache key prefix constants tests
234    #[test]
235    fn test_key_constants() {
236        assert_eq!(keys::ORG, "cache:org:");
237        assert_eq!(keys::USER, "cache:user:");
238        assert_eq!(keys::ORG_SETTING, "cache:org_setting:");
239        assert_eq!(keys::USER_SETTING, "cache:user_setting:");
240        assert_eq!(keys::SUBSCRIPTION, "cache:subscription:");
241        assert_eq!(keys::PLUGIN, "cache:plugin:");
242        assert_eq!(keys::TEMPLATE, "cache:template:");
243        assert_eq!(keys::SCENARIO, "cache:scenario:");
244        assert_eq!(keys::ORG_MEMBERS, "cache:org_members:");
245    }
246
247    // TTL constants tests
248    #[test]
249    fn test_ttl_short() {
250        assert_eq!(ttl::SHORT, 60);
251    }
252
253    #[test]
254    fn test_ttl_medium() {
255        assert_eq!(ttl::MEDIUM, 300);
256    }
257
258    #[test]
259    fn test_ttl_long() {
260        assert_eq!(ttl::LONG, 900);
261    }
262
263    #[test]
264    fn test_ttl_very_long() {
265        assert_eq!(ttl::VERY_LONG, 3600);
266    }
267
268    #[test]
269    fn test_ttl_org() {
270        assert_eq!(ttl::ORG, ttl::MEDIUM);
271    }
272
273    #[test]
274    fn test_ttl_user() {
275        assert_eq!(ttl::USER, ttl::MEDIUM);
276    }
277
278    #[test]
279    fn test_ttl_settings() {
280        assert_eq!(ttl::SETTINGS, ttl::LONG);
281    }
282
283    #[test]
284    fn test_ttl_subscription() {
285        assert_eq!(ttl::SUBSCRIPTION, ttl::MEDIUM);
286    }
287
288    #[test]
289    fn test_ttl_marketplace() {
290        assert_eq!(ttl::MARKETPLACE, ttl::LONG);
291    }
292
293    #[test]
294    fn test_ttl_org_members() {
295        assert_eq!(ttl::ORG_MEMBERS, ttl::MEDIUM);
296    }
297
298    // Cache key helper function tests
299    #[test]
300    fn test_org_cache_key() {
301        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
302        let key = org_cache_key(&id);
303        assert_eq!(key, "cache:org::550e8400-e29b-41d4-a716-446655440000");
304    }
305
306    #[test]
307    fn test_user_cache_key() {
308        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440001").unwrap();
309        let key = user_cache_key(&id);
310        assert_eq!(key, "cache:user::550e8400-e29b-41d4-a716-446655440001");
311    }
312
313    #[test]
314    fn test_org_setting_cache_key() {
315        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440002").unwrap();
316        let key = org_setting_cache_key(&id, "theme");
317        assert_eq!(key, "cache:org_setting::550e8400-e29b-41d4-a716-446655440002:theme");
318    }
319
320    #[test]
321    fn test_user_setting_cache_key() {
322        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440003").unwrap();
323        let key = user_setting_cache_key(&id, "notifications");
324        assert_eq!(key, "cache:user_setting::550e8400-e29b-41d4-a716-446655440003:notifications");
325    }
326
327    #[test]
328    fn test_subscription_cache_key() {
329        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440004").unwrap();
330        let key = subscription_cache_key(&id);
331        assert_eq!(key, "cache:subscription::550e8400-e29b-41d4-a716-446655440004");
332    }
333
334    #[test]
335    fn test_org_members_cache_key() {
336        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440005").unwrap();
337        let key = org_members_cache_key(&id);
338        assert_eq!(key, "cache:org_members::550e8400-e29b-41d4-a716-446655440005");
339    }
340
341    #[test]
342    fn test_plugin_cache_key() {
343        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440006").unwrap();
344        let key = plugin_cache_key(&id);
345        assert_eq!(key, "cache:plugin::550e8400-e29b-41d4-a716-446655440006");
346    }
347
348    #[test]
349    fn test_template_cache_key() {
350        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440007").unwrap();
351        let key = template_cache_key(&id);
352        assert_eq!(key, "cache:template::550e8400-e29b-41d4-a716-446655440007");
353    }
354
355    #[test]
356    fn test_scenario_cache_key() {
357        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440008").unwrap();
358        let key = scenario_cache_key(&id);
359        assert_eq!(key, "cache:scenario::550e8400-e29b-41d4-a716-446655440008");
360    }
361
362    // Test key uniqueness
363    #[test]
364    fn test_cache_keys_are_unique() {
365        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
366
367        let org_key = org_cache_key(&id);
368        let user_key = user_cache_key(&id);
369        let plugin_key = plugin_cache_key(&id);
370        let template_key = template_cache_key(&id);
371        let scenario_key = scenario_cache_key(&id);
372        let subscription_key = subscription_cache_key(&id);
373
374        // All keys should be different even for same UUID
375        assert_ne!(org_key, user_key);
376        assert_ne!(org_key, plugin_key);
377        assert_ne!(plugin_key, template_key);
378        assert_ne!(template_key, scenario_key);
379        assert_ne!(subscription_key, org_key);
380    }
381
382    // Test setting key variations
383    #[test]
384    fn test_setting_keys_with_different_settings() {
385        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
386
387        let theme_key = org_setting_cache_key(&id, "theme");
388        let lang_key = org_setting_cache_key(&id, "language");
389
390        assert_ne!(theme_key, lang_key);
391        assert!(theme_key.contains("theme"));
392        assert!(lang_key.contains("language"));
393    }
394}