tibba_cache/ttl_lru_store.rs
1// Copyright 2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use lru::LruCache;
16use std::num::NonZeroUsize;
17use tokio::sync::RwLock;
18
19/// Trait for types that can expire
20pub trait Expired {
21 /// Checks if the data has expired
22 /// # Returns
23 /// * `true` - The data has expired and should be removed
24 /// * `false` - The data is still valid
25 fn is_expired(&self) -> bool;
26}
27
28/// A thread-safe storage component combining TTL (Time-To-Live) and LRU (Least Recently Used) caching strategies
29/// # Type Parameters
30/// * `T` - The type of values to store, must implement Expired trait
31pub struct TtlLruStore<T> {
32 /// Thread-safe LRU cache storing key-value pairs
33 /// Uses RwLock for concurrent access with multiple readers or single writer
34 cache: RwLock<LruCache<String, T>>,
35}
36
37impl<T: Expired + Clone> TtlLruStore<T> {
38 /// Creates a new TtlLruStore with specified capacity
39 /// # Arguments
40 /// * `size` - Maximum number of items the cache can hold (must be non-zero)
41 /// # Returns
42 /// * A new TtlLruStore instance
43 pub fn new(size: NonZeroUsize) -> Self {
44 let cache: LruCache<String, T> = LruCache::new(size);
45 TtlLruStore {
46 cache: RwLock::new(cache),
47 }
48 }
49
50 /// Stores a value in the cache
51 /// # Arguments
52 /// * `key` - The key under which to store the value
53 /// * `value` - The value to store
54 /// # Notes
55 /// * If the cache is at capacity, the least recently used item will be removed
56 /// * If the key already exists, the value will be updated
57 pub async fn set(&self, key: &str, value: T) {
58 let cache = &mut self.cache.write().await;
59 cache.put(key.to_string(), value);
60 }
61
62 /// Retrieves a value from the cache if it exists and hasn't expired
63 /// # Arguments
64 /// * `key` - The key to look up
65 /// # Returns
66 /// * `Some(T)` - The value if found and not expired
67 /// * `None` - If key doesn't exist or value has expired
68 /// # Notes
69 /// * Uses peek() instead of get() to avoid updating LRU order
70 /// * Returns a clone of the value to maintain thread safety
71 pub async fn get(&self, key: &str) -> Option<T> {
72 let cache = self.cache.read().await;
73 // better performance use peek to avoid moving the data to the front of the cache
74 let v = cache.peek(key)?;
75 if !v.is_expired() {
76 return Some(v.clone());
77 }
78 None
79 }
80
81 /// Removes a value from the cache
82 /// # Arguments
83 /// * `key` - The key to remove
84 /// # Notes
85 /// * No-op if key doesn't exist
86 /// * Requires mutable access to the store
87 pub async fn del(&self, key: &str) {
88 let mut cache = self.cache.write().await;
89 cache.pop(key);
90 }
91
92 /// This method should be called periodically to free up memory from expired "garbage" data.
93 pub async fn purge_expired(&self) {
94 let mut cache = self.cache.write().await;
95 // LruCache a a limited API for removal during iteration.
96 // The safest way is to collect keys and then remove them.
97 let keys_to_remove: Vec<String> = cache
98 .iter()
99 .filter(|(_, v)| v.is_expired())
100 .map(|(k, _)| k.clone())
101 .collect();
102
103 if keys_to_remove.is_empty() {
104 return;
105 }
106
107 for key in keys_to_remove {
108 cache.pop(&key);
109 }
110 }
111}