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}