open_lark/core/
cache.rs

1use std::time::Duration;
2
3use quick_cache::sync::Cache;
4use tokio::time::Instant;
5
6#[derive(Debug)]
7pub struct QuickCache<T: Clone> {
8    cache: Cache<String, (T, Instant)>,
9}
10
11impl<T: Clone> Default for QuickCache<T> {
12    fn default() -> Self {
13        Self::new()
14    }
15}
16
17impl<T: Clone> QuickCache<T> {
18    pub fn new() -> Self {
19        let cache = Cache::new(10);
20        Self { cache } //
21    }
22
23    pub fn get(&self, key: &str) -> Option<T> {
24        match self.cache.get(key) {
25            None => None,
26            Some((value, expire_time)) => {
27                if expire_time > Instant::now() {
28                    Some(value)
29                } else {
30                    self.cache.remove(key);
31                    None
32                }
33            }
34        }
35    }
36
37    /// Set a key-value pair with an expire time in seconds.
38    pub fn set(&mut self, key: &str, value: T, expire_time: i32) {
39        let expire_time = Instant::now() + Duration::from_secs(expire_time as u64);
40        self.cache.insert(key.to_string(), (value, expire_time));
41    }
42
43    /// 获取值和过期信息
44    pub fn get_with_expiry(&self, key: &str) -> Option<CacheEntry<T>> {
45        match self.cache.get(key) {
46            None => None,
47            Some((value, expire_time)) => {
48                let now = Instant::now();
49                if expire_time > now {
50                    Some(CacheEntry {
51                        value,
52                        expires_at: expire_time,
53                        current_time: now,
54                    })
55                } else {
56                    self.cache.remove(key);
57                    None
58                }
59            }
60        }
61    }
62}
63
64/// 缓存条目信息,包含值和过期时间
65#[derive(Debug, Clone)]
66pub struct CacheEntry<T> {
67    pub value: T,
68    pub expires_at: Instant,
69    pub current_time: Instant,
70}
71
72impl<T> CacheEntry<T> {
73    /// 获取剩余的过期秒数
74    pub fn expiry_seconds(&self) -> u64 {
75        if self.expires_at > self.current_time {
76            (self.expires_at - self.current_time).as_secs()
77        } else {
78            0
79        }
80    }
81
82    /// 检查是否即将过期(剩余时间少于指定秒数)
83    pub fn expires_within(&self, seconds: u64) -> bool {
84        self.expiry_seconds() <= seconds
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use std::thread::sleep;
92    use std::time::Duration;
93
94    #[test]
95    fn test_cache_creation() {
96        let cache: QuickCache<String> = QuickCache::new();
97
98        // Test that cache is empty initially
99        assert!(cache.get("test_key").is_none());
100    }
101
102    #[test]
103    fn test_cache_default() {
104        let cache: QuickCache<i32> = QuickCache::default();
105
106        // Test default implementation
107        assert!(cache.get("test_key").is_none());
108    }
109
110    #[test]
111    fn test_cache_set_and_get() {
112        let mut cache = QuickCache::new();
113
114        // Set a value
115        cache.set("key1", "value1".to_string(), 10);
116
117        // Get the value
118        let result = cache.get("key1");
119        assert_eq!(result, Some("value1".to_string()));
120    }
121
122    #[test]
123    fn test_cache_expiration() {
124        let mut cache = QuickCache::new();
125
126        // Set a value with very short expiration
127        cache.set("short_key", "short_value".to_string(), 1);
128
129        // Value should be available immediately
130        assert_eq!(cache.get("short_key"), Some("short_value".to_string()));
131
132        // Wait for expiration
133        sleep(Duration::from_secs(2));
134
135        // Value should be expired and removed
136        assert!(cache.get("short_key").is_none());
137    }
138
139    #[test]
140    fn test_cache_nonexistent_key() {
141        let cache: QuickCache<String> = QuickCache::new();
142
143        // Getting non-existent key should return None
144        assert!(cache.get("nonexistent").is_none());
145    }
146
147    #[test]
148    fn test_cache_with_expiry_info() {
149        let mut cache = QuickCache::new();
150
151        // Set a value with longer expiration
152        cache.set("expiry_key", 42, 60);
153
154        // Get with expiry info
155        let entry = cache.get_with_expiry("expiry_key");
156        assert!(entry.is_some());
157
158        let entry = entry.unwrap();
159        assert_eq!(entry.value, 42);
160        assert!(entry.expiry_seconds() > 0);
161        assert!(entry.expiry_seconds() <= 60);
162    }
163
164    #[test]
165    fn test_cache_entry_expiry_seconds() {
166        let mut cache = QuickCache::new();
167
168        // Set value with 30 second expiration
169        cache.set("timing_key", "timing_value".to_string(), 30);
170
171        let entry = cache.get_with_expiry("timing_key").unwrap();
172        let remaining = entry.expiry_seconds();
173
174        // Should have approximately 30 seconds remaining (allowing for slight timing variations)
175        assert!(remaining > 25 && remaining <= 30);
176    }
177
178    #[test]
179    fn test_cache_entry_expires_within() {
180        let mut cache = QuickCache::new();
181
182        // Set value with 5 second expiration
183        cache.set("soon_key", "soon_value".to_string(), 5);
184
185        let entry = cache.get_with_expiry("soon_key").unwrap();
186
187        // Should expire within 10 seconds
188        assert!(entry.expires_within(10));
189
190        // Should not expire within 1 second
191        assert!(!entry.expires_within(1));
192    }
193
194    #[test]
195    fn test_cache_with_different_types() {
196        let mut string_cache = QuickCache::new();
197        let mut int_cache = QuickCache::new();
198        let mut vec_cache = QuickCache::new();
199
200        // Test with String
201        string_cache.set("str_key", "hello".to_string(), 30);
202        assert_eq!(string_cache.get("str_key"), Some("hello".to_string()));
203
204        // Test with integer
205        int_cache.set("int_key", 123, 30);
206        assert_eq!(int_cache.get("int_key"), Some(123));
207
208        // Test with Vec
209        vec_cache.set("vec_key", vec![1, 2, 3], 30);
210        assert_eq!(vec_cache.get("vec_key"), Some(vec![1, 2, 3]));
211    }
212
213    #[test]
214    fn test_cache_overwrite_key() {
215        let mut cache = QuickCache::new();
216
217        // Set initial value
218        cache.set("overwrite_key", "first_value".to_string(), 30);
219        assert_eq!(cache.get("overwrite_key"), Some("first_value".to_string()));
220
221        // Overwrite with new value
222        cache.set("overwrite_key", "second_value".to_string(), 30);
223        assert_eq!(cache.get("overwrite_key"), Some("second_value".to_string()));
224    }
225
226    #[test]
227    fn test_cache_multiple_keys() {
228        let mut cache = QuickCache::new();
229
230        // Set multiple key-value pairs
231        cache.set("key1", "value1".to_string(), 60);
232        cache.set("key2", "value2".to_string(), 60);
233        cache.set("key3", "value3".to_string(), 60);
234
235        // Verify all can be retrieved
236        assert_eq!(cache.get("key1"), Some("value1".to_string()));
237        assert_eq!(cache.get("key2"), Some("value2".to_string()));
238        assert_eq!(cache.get("key3"), Some("value3".to_string()));
239    }
240
241    #[test]
242    fn test_cache_expired_entry_cleanup() {
243        let mut cache = QuickCache::new();
244
245        // Set a value that expires quickly
246        cache.set("cleanup_key", "cleanup_value".to_string(), 1);
247
248        // Verify it exists
249        assert!(cache.get("cleanup_key").is_some());
250
251        // Wait for expiration
252        sleep(Duration::from_secs(2));
253
254        // Accessing expired key should remove it and return None
255        assert!(cache.get("cleanup_key").is_none());
256
257        // Subsequent access should still return None
258        assert!(cache.get("cleanup_key").is_none());
259    }
260
261    #[test]
262    fn test_cache_entry_debug_trait() {
263        let mut cache = QuickCache::new();
264        cache.set("debug_key", "debug_value".to_string(), 60);
265
266        let entry = cache.get_with_expiry("debug_key").unwrap();
267        let debug_string = format!("{:?}", entry);
268
269        assert!(debug_string.contains("CacheEntry"));
270        assert!(debug_string.contains("debug_value"));
271    }
272
273    #[test]
274    fn test_cache_debug_trait() {
275        let cache: QuickCache<String> = QuickCache::new();
276        let debug_string = format!("{:?}", cache);
277
278        assert!(debug_string.contains("QuickCache"));
279    }
280
281    #[test]
282    fn test_zero_expiry_seconds() {
283        let now = Instant::now();
284        let past_time = now - Duration::from_secs(10);
285
286        let entry = CacheEntry {
287            value: "test".to_string(),
288            expires_at: past_time,
289            current_time: now,
290        };
291
292        // Should return 0 for expired entries
293        assert_eq!(entry.expiry_seconds(), 0);
294        assert!(entry.expires_within(100));
295    }
296
297    #[test]
298    fn test_cache_entry_clone() {
299        let mut cache = QuickCache::new();
300        cache.set("clone_key", vec![1, 2, 3], 60);
301
302        let entry = cache.get_with_expiry("clone_key").unwrap();
303        let cloned_entry = entry.clone();
304
305        assert_eq!(entry.value, cloned_entry.value);
306        assert_eq!(entry.expires_at, cloned_entry.expires_at);
307        assert_eq!(entry.current_time, cloned_entry.current_time);
308    }
309
310    #[test]
311    fn test_cache_with_zero_expiry() {
312        let mut cache = QuickCache::new();
313
314        // Set value with 0 second expiry (should expire immediately)
315        cache.set("zero_key", "zero_value".to_string(), 0);
316
317        // Value should not be retrievable due to immediate expiry
318        assert!(cache.get("zero_key").is_none());
319    }
320
321    #[test]
322    fn test_cache_with_negative_expiry() {
323        let mut cache = QuickCache::new();
324
325        // Set value with negative expiry - this should be handled gracefully
326        // Note: casting -1 as u64 creates a very large number, but adding it to Instant can overflow
327        // Let's test with 0 instead to simulate immediate expiry
328        cache.set("zero_expiry_key", "zero_value".to_string(), 0);
329
330        // Value with 0 expiry should not be retrievable due to immediate expiry
331        assert!(cache.get("zero_expiry_key").is_none());
332    }
333
334    #[test]
335    fn test_cache_large_expiry_time() {
336        let mut cache = QuickCache::new();
337
338        // Set value with very large expiry time
339        cache.set("large_key", "large_value".to_string(), i32::MAX);
340
341        let entry = cache.get_with_expiry("large_key").unwrap();
342        assert_eq!(entry.value, "large_value");
343        assert!(entry.expiry_seconds() > 1_000_000);
344    }
345
346    #[test]
347    fn test_get_with_expiry_nonexistent_key() {
348        let cache: QuickCache<String> = QuickCache::new();
349
350        // Getting non-existent key with expiry should return None
351        assert!(cache.get_with_expiry("nonexistent").is_none());
352    }
353
354    #[test]
355    fn test_cache_with_empty_strings() {
356        let mut cache = QuickCache::new();
357
358        // Test with empty key
359        cache.set("", "empty_key_value".to_string(), 30);
360        assert_eq!(cache.get(""), Some("empty_key_value".to_string()));
361
362        // Test with empty value
363        cache.set("empty_value_key", "".to_string(), 30);
364        assert_eq!(cache.get("empty_value_key"), Some("".to_string()));
365
366        // Test with both empty
367        cache.set("", "".to_string(), 30);
368        assert_eq!(cache.get(""), Some("".to_string()));
369    }
370
371    #[test]
372    fn test_cache_with_unicode_strings() {
373        let mut cache = QuickCache::new();
374
375        let unicode_key = "键名_🔑";
376        let unicode_value = "值_🎯_测试";
377
378        cache.set(unicode_key, unicode_value.to_string(), 30);
379        assert_eq!(cache.get(unicode_key), Some(unicode_value.to_string()));
380    }
381
382    #[test]
383    fn test_cache_with_long_strings() {
384        let mut cache = QuickCache::new();
385
386        let long_key = "a".repeat(1000);
387        let long_value = "b".repeat(10000);
388
389        cache.set(&long_key, long_value.clone(), 30);
390        assert_eq!(cache.get(&long_key), Some(long_value));
391    }
392
393    #[test]
394    fn test_cache_memory_efficiency() {
395        let mut cache = QuickCache::new();
396
397        // Add items within cache capacity (cache has limit of 10)
398        for i in 0..8 {
399            cache.set(&format!("key_{}", i), format!("value_{}", i), 60);
400        }
401
402        // Verify items within capacity are accessible
403        for i in 0..8 {
404            let key = format!("key_{}", i);
405            let expected_value = format!("value_{}", i);
406            assert_eq!(cache.get(&key), Some(expected_value));
407        }
408    }
409
410    #[test]
411    fn test_cache_concurrent_style_simulation() {
412        // Simulate concurrent-style access patterns (without actual threads for determinism)
413        let mut cache = QuickCache::new();
414
415        // Simulate limited operations within cache capacity
416        for thread_id in 0..2 {
417            for operation in 0..3 {
418                let key = format!("thread_{}_op_{}", thread_id, operation);
419                let value = format!("data_{}", thread_id * 3 + operation);
420
421                cache.set(&key, value.clone(), 30);
422                assert_eq!(cache.get(&key), Some(value));
423            }
424        }
425
426        // Verify data within capacity is still accessible
427        for thread_id in 0..2 {
428            for operation in 0..3 {
429                let key = format!("thread_{}_op_{}", thread_id, operation);
430                let expected_value = format!("data_{}", thread_id * 3 + operation);
431                assert_eq!(cache.get(&key), Some(expected_value));
432            }
433        }
434    }
435
436    #[test]
437    fn test_cache_entry_exact_timing() {
438        // Test entry timing precision
439        let now = Instant::now();
440        let expires_in_5_seconds = now + Duration::from_secs(5);
441
442        let entry = CacheEntry {
443            value: "timing_test".to_string(),
444            expires_at: expires_in_5_seconds,
445            current_time: now,
446        };
447
448        assert_eq!(entry.expiry_seconds(), 5);
449        assert!(entry.expires_within(5)); // Should expire exactly at threshold
450        assert!(entry.expires_within(6)); // Should expire before 6 seconds
451        assert!(!entry.expires_within(4)); // Should not expire before 4 seconds
452    }
453
454    #[test]
455    fn test_cache_entry_boundary_conditions() {
456        let now = Instant::now();
457
458        // Test exactly at boundary
459        let exactly_now = CacheEntry {
460            value: "now".to_string(),
461            expires_at: now,
462            current_time: now,
463        };
464        assert_eq!(exactly_now.expiry_seconds(), 0);
465        assert!(exactly_now.expires_within(0));
466        assert!(exactly_now.expires_within(1));
467
468        // Test 1 nanosecond future
469        let one_nano_future = CacheEntry {
470            value: "nano".to_string(),
471            expires_at: now + Duration::from_nanos(1),
472            current_time: now,
473        };
474        assert_eq!(one_nano_future.expiry_seconds(), 0); // Rounds down to 0
475        assert!(one_nano_future.expires_within(1));
476    }
477
478    #[test]
479    fn test_cache_with_complex_types() {
480        use std::collections::HashMap;
481
482        // Test with HashMap
483        let mut map_cache = QuickCache::new();
484        let mut map = HashMap::new();
485        map.insert("nested_key".to_string(), "nested_value".to_string());
486
487        map_cache.set("map_key", map.clone(), 30);
488        assert_eq!(map_cache.get("map_key"), Some(map));
489
490        // Test with Option
491        let mut option_cache = QuickCache::new();
492        option_cache.set("some_key", Some(42), 30);
493        assert_eq!(option_cache.get("some_key"), Some(Some(42)));
494
495        option_cache.set("none_key", None::<i32>, 30);
496        assert_eq!(option_cache.get("none_key"), Some(None));
497    }
498
499    #[test]
500    fn test_get_with_expiry_expired_cleanup() {
501        let mut cache = QuickCache::new();
502
503        // Set a value that expires quickly
504        cache.set("expiry_cleanup", "will_expire".to_string(), 1);
505
506        // Verify it exists with expiry info
507        assert!(cache.get_with_expiry("expiry_cleanup").is_some());
508
509        // Wait for expiration
510        sleep(Duration::from_secs(2));
511
512        // get_with_expiry should also cleanup expired entries
513        assert!(cache.get_with_expiry("expiry_cleanup").is_none());
514
515        // Subsequent calls should also return None
516        assert!(cache.get_with_expiry("expiry_cleanup").is_none());
517    }
518}