pingap_core/
ttl_lru_limit.rs

1// Copyright 2024-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 super::{now_ms, LOG_CATEGORY};
16use std::time::Duration;
17use tinyufo::TinyUfo;
18use tracing::debug;
19
20#[derive(Debug, Clone)]
21struct TtlLimit {
22    count: usize,
23    created_at: u64,
24}
25
26pub struct TtlLruLimit {
27    ttl: u64,
28    ufo: TinyUfo<String, TtlLimit>,
29    max: usize,
30}
31
32impl TtlLruLimit {
33    /// Creates a new TTL-based LRU limit with the specified parameters.
34    ///
35    /// # Arguments
36    ///
37    /// * `size` - The maximum number of entries to store in the LRU cache
38    /// * `ttl` - The time-to-live duration after which entries are considered expired
39    /// * `max` - The maximum count allowed per key within the TTL window
40    pub fn new(size: usize, ttl: Duration, max: usize) -> Self {
41        Self {
42            ttl: ttl.as_millis() as u64,
43            max,
44            ufo: TinyUfo::new(size, size),
45        }
46    }
47    /// Creates a new compact TTL-based LRU limit with the specified parameters.
48    ///
49    /// # Arguments
50    ///
51    /// * `size` - The maximum number of entries to store in the LRU cache
52    /// * `ttl` - The time-to-live duration after which entries are considered expired
53    pub fn new_compact(size: usize, ttl: Duration, max: usize) -> Self {
54        Self {
55            ttl: ttl.as_millis() as u64,
56            max,
57            ufo: TinyUfo::new_compact(size, size),
58        }
59    }
60    /// Validates whether a key has not exceeded its rate limit.
61    ///
62    /// # Arguments
63    ///
64    /// * `key` - The key to validate
65    ///
66    /// # Returns
67    ///
68    /// Returns `true` if the key is within its limit or has expired, `false` otherwise.
69    pub fn validate(&self, key: &str) -> bool {
70        let mut should_reset = false;
71        let mut valid = false;
72        let key = key.to_string();
73
74        if let Some(value) = self.ufo.get(&key) {
75            debug!(
76                category = LOG_CATEGORY,
77                key,
78                value = format!("{value:?}"),
79                "ttl lru limit"
80            );
81            // validate expired first
82            if now_ms() - value.created_at > self.ttl {
83                valid = true;
84                should_reset = true;
85            } else if value.count < self.max {
86                valid = true;
87            }
88        } else {
89            valid = true
90        }
91        if should_reset {
92            // ufo does not support remove
93            self.ufo.put(
94                key,
95                TtlLimit {
96                    count: 0,
97                    created_at: 0,
98                },
99                1,
100            );
101        }
102
103        valid
104    }
105    /// Increments the counter for the specified key.
106    /// If the key doesn't exist, creates a new entry with count 1.
107    /// If the key exists but was reset (count = 0), updates its creation timestamp.
108    ///
109    /// # Arguments
110    ///
111    /// * `key` - The key to increment
112    pub fn inc(&self, key: &str) {
113        let key = key.to_string();
114        let data = if let Some(mut value) = self.ufo.get(&key) {
115            // the reset value
116            if value.created_at == 0 {
117                value.created_at = now_ms();
118            }
119            value.count += 1;
120            value
121        } else {
122            TtlLimit {
123                count: 1,
124                created_at: now_ms(),
125            }
126        };
127        self.ufo.put(key, data, 1);
128    }
129}
130
131#[cfg(test)]
132mod test {
133    use super::TtlLruLimit;
134    use pretty_assertions::assert_eq;
135    use std::time::Duration;
136
137    #[test]
138    fn test_ttl_lru_limit() {
139        let limit = TtlLruLimit::new(5, Duration::from_millis(500), 3);
140
141        let key = "abc";
142        assert_eq!(true, limit.validate(key));
143        limit.inc(key);
144        limit.inc(key);
145        assert_eq!(true, limit.validate(key));
146        limit.inc(key);
147        assert_eq!(false, limit.validate(key));
148        std::thread::sleep(Duration::from_millis(600));
149        assert_eq!(true, limit.validate(key));
150    }
151}