Skip to main content

over_there/utils/
ttl.rs

1use std::cmp::Ordering;
2use std::hash::{Hash, Hasher};
3use std::ops::{Deref, DerefMut};
4use std::time::Duration;
5use std::time::Instant;
6
7/// Represents a value that has a limited lifetime before expiring
8#[derive(Debug)]
9pub struct TtlValue<T> {
10    pub value: T,
11    last_touched: Instant,
12    ttl: Duration,
13}
14
15/// Represents a void value, purely used to keep track of access times
16pub type EmptyTtlValue = TtlValue<()>;
17
18impl EmptyTtlValue {
19    pub fn empty(ttl: Duration) -> Self {
20        Self::new((), ttl)
21    }
22}
23
24impl<T> TtlValue<T> {
25    pub fn new(value: T, ttl: Duration) -> Self {
26        Self {
27            value,
28            last_touched: Instant::now(),
29            ttl,
30        }
31    }
32
33    pub fn touch(&mut self) {
34        self.last_touched = Instant::now();
35    }
36
37    pub fn last_touched(&self) -> &Instant {
38        &self.last_touched
39    }
40
41    pub fn ttl(&self) -> &Duration {
42        &self.ttl
43    }
44
45    pub fn has_expired(&self) -> bool {
46        self.last_touched.elapsed().checked_sub(self.ttl).is_some()
47    }
48}
49
50impl<T: Hash> Hash for TtlValue<T> {
51    fn hash<H: Hasher>(&self, state: &mut H) {
52        self.value.hash(state);
53    }
54}
55
56impl<T: Eq> Eq for TtlValue<T> {}
57
58impl<T: Eq> PartialEq for TtlValue<T> {
59    fn eq(&self, other: &Self) -> bool {
60        self.value == other.value
61    }
62}
63
64impl<T: PartialOrd + Eq> PartialOrd for TtlValue<T> {
65    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
66        self.value.partial_cmp(&other.value)
67    }
68}
69
70impl<T: Ord> Ord for TtlValue<T> {
71    fn cmp(&self, other: &Self) -> Ordering {
72        self.value.cmp(&other.value)
73    }
74}
75
76impl<T: Clone> Clone for TtlValue<T> {
77    fn clone(&self) -> Self {
78        Self {
79            value: self.value.clone(),
80            last_touched: self.last_touched,
81            ttl: self.ttl,
82        }
83    }
84}
85
86impl<T: Copy> Copy for TtlValue<T> {}
87
88impl<T> From<T> for TtlValue<T> {
89    fn from(value: T) -> Self {
90        Self::new(value, Duration::new(0, 0))
91    }
92}
93
94impl<T> Into<Instant> for TtlValue<T> {
95    fn into(self) -> Instant {
96        self.last_touched
97    }
98}
99
100impl<T> Into<Duration> for TtlValue<T> {
101    fn into(self) -> Duration {
102        self.ttl
103    }
104}
105
106impl<T> Deref for TtlValue<T> {
107    type Target = T;
108
109    fn deref(&self) -> &Self::Target {
110        &self.value
111    }
112}
113
114impl<T> DerefMut for TtlValue<T> {
115    fn deref_mut(&mut self) -> &mut Self::Target {
116        &mut self.value
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use std::collections::HashSet;
124
125    #[test]
126    fn touch_should_renew_lifetime_of_value() {
127        let mut ttl_value = TtlValue::new(0, Duration::from_millis(100));
128
129        // Make the last touched time be in the past where we exceed the TTL
130        ttl_value.last_touched = Instant::now()
131            .checked_sub(Duration::from_millis(1000))
132            .unwrap();
133
134        // Now refresh
135        ttl_value.touch();
136
137        assert!(!ttl_value.has_expired(), "Value expired unexpectedly");
138    }
139
140    #[test]
141    fn has_expired_should_return_false_if_value_has_not_expired() {
142        let ttl_value = TtlValue::new(0, Duration::from_millis(100));
143
144        assert!(!ttl_value.has_expired(), "Value expired unexpectedly");
145    }
146
147    #[test]
148    fn has_expired_should_return_true_if_value_has_expired() {
149        let mut ttl_value = TtlValue::new(0, Duration::from_millis(100));
150
151        // Make the last touched time be in the past where we exceed the TTL
152        ttl_value.last_touched = Instant::now()
153            .checked_sub(Duration::from_millis(1000))
154            .unwrap();
155
156        assert!(ttl_value.has_expired(), "Value not expired when should be");
157    }
158
159    #[test]
160    fn hash_should_use_underlying_value() {
161        // Values do not overlap, so creates two entries
162        let mut set = HashSet::new();
163        let v1 = TtlValue::new(0, Duration::from_millis(5));
164        let v2 = TtlValue::new(1, Duration::from_millis(5));
165
166        set.insert(v1);
167        set.insert(v2);
168
169        assert_eq!(set.len(), 2);
170        assert!(set.get(&0.into()).is_some());
171        assert!(set.get(&1.into()).is_some());
172
173        // Values do overlap, so creates one entry
174        let mut set = HashSet::new();
175        let v1 = TtlValue::new(2, Duration::from_millis(5));
176        let v2 = TtlValue::new(2, Duration::from_millis(10));
177
178        set.insert(v1);
179        set.insert(v2);
180
181        assert_eq!(set.len(), 1);
182        assert!(set.get(&2.into()).is_some());
183    }
184
185    #[test]
186    fn partial_eq_should_use_underlying_value() {
187        let v1 = TtlValue::new(5, Duration::from_millis(1));
188        let v2 = TtlValue::new(5, Duration::from_millis(2));
189
190        assert_eq!(v1, v2);
191    }
192
193    #[test]
194    fn partial_ord_should_use_underlying_value() {
195        let v1 = TtlValue::new(3, Duration::from_millis(1));
196        let v2 = TtlValue::new(5, Duration::from_millis(1));
197
198        assert!(v1 < v2);
199    }
200
201    #[test]
202    fn clone_should_use_underlying_value() {
203        let v1 = TtlValue::new(3, Duration::from_millis(1));
204
205        assert_eq!(v1, v1.clone());
206    }
207
208    #[test]
209    fn copy_should_use_underlying_value() {
210        let v1 = TtlValue::new(3, Duration::from_millis(1));
211        let v2 = v1;
212
213        assert_eq!(v1, v2);
214    }
215
216    #[test]
217    fn from_should_use_underlying_value_and_produce_a_duration_of_zero() {
218        let v1 = TtlValue::from(3);
219
220        assert_eq!(v1.value, 3);
221        assert_eq!(v1.ttl, Duration::new(0, 0));
222    }
223
224    #[test]
225    fn into_should_return_last_touched_if_type_is_instant() {
226        let v1 = TtlValue::new(3, Duration::from_millis(5));
227        let v2 = v1.clone();
228
229        let i: Instant = v1.into();
230        assert_eq!(i, v2.last_touched);
231    }
232
233    #[test]
234    fn into_should_return_ttl_if_type_is_duration() {
235        let v1 = TtlValue::new(3, Duration::from_millis(5));
236
237        let d: Duration = v1.into();
238        assert_eq!(d, Duration::from_millis(5));
239    }
240
241    #[test]
242    fn deref_should_yield_underlying_value() {
243        let v1 = TtlValue::new(5, Duration::from_millis(1));
244
245        assert_eq!(*v1, 5);
246    }
247
248    #[test]
249    fn deref_mut_should_yield_underlying_value() {
250        let mut v1 = TtlValue::new(5, Duration::from_millis(1));
251
252        *v1 = 10;
253
254        assert_eq!(*v1, 10);
255    }
256}