1pub const MAX_TTL_TIER: u8 = 63;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
28pub struct CacheTier(u8);
29
30impl CacheTier {
31 #[must_use]
32 pub const fn new(value: u8) -> Self {
33 Self(value)
34 }
35
36 #[must_use]
37 pub const fn get(self) -> u8 {
38 self.0
39 }
40}
41
42impl From<u8> for CacheTier {
43 fn from(value: u8) -> Self {
44 Self::new(value)
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
50pub struct CacheTimestampMillis(u64);
51
52impl CacheTimestampMillis {
53 #[must_use]
54 pub const fn new(value: u64) -> Self {
55 Self(value)
56 }
57
58 #[must_use]
59 pub const fn get(self) -> u64 {
60 self.0
61 }
62
63 #[must_use]
64 pub fn now() -> Self {
65 Self(now_millis())
66 }
67}
68
69impl From<u64> for CacheTimestampMillis {
70 fn from(value: u64) -> Self {
71 Self::new(value)
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
77pub struct CacheTtlMillis(u64);
78
79impl CacheTtlMillis {
80 #[must_use]
81 pub const fn new(value: u64) -> Self {
82 Self(value)
83 }
84
85 #[must_use]
86 pub const fn get(self) -> u64 {
87 self.0
88 }
89
90 #[must_use]
91 pub fn from_duration(duration: std::time::Duration) -> Self {
92 Self(duration_millis_u64(duration))
93 }
94}
95
96impl From<u64> for CacheTtlMillis {
97 fn from(value: u64) -> Self {
98 Self::new(value)
99 }
100}
101
102#[inline]
116#[must_use]
117pub const fn effective_ttl(base_ttl: CacheTtlMillis, tier: CacheTier) -> CacheTtlMillis {
118 let tier = tier.get();
119 let capped_tier = if tier > MAX_TTL_TIER {
120 MAX_TTL_TIER
121 } else {
122 tier
123 };
124 CacheTtlMillis::new(base_ttl.get().saturating_mul(1u64 << capped_tier))
125}
126
127#[inline]
129#[must_use]
130pub fn now_millis() -> u64 {
131 std::time::SystemTime::now()
132 .duration_since(std::time::UNIX_EPOCH)
133 .map_or(0, duration_millis_u64)
134}
135
136#[allow(clippy::cast_possible_truncation)] fn duration_millis_u64(duration: std::time::Duration) -> u64 {
138 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
141}
142
143#[inline]
162#[must_use]
163pub fn is_expired(
164 inserted_at: CacheTimestampMillis,
165 base_ttl: CacheTtlMillis,
166 tier: CacheTier,
167) -> bool {
168 let elapsed = now_millis().saturating_sub(inserted_at.get());
169 elapsed >= effective_ttl(base_ttl, tier).get()
170}
171
172#[inline]
184#[must_use]
185pub const fn ttl_multiplier(tier: CacheTier) -> u64 {
186 let tier = tier.get();
187 let capped = if tier > MAX_TTL_TIER {
188 MAX_TTL_TIER
189 } else {
190 tier
191 };
192 1u64 << capped
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 const fn ttl(value: u64) -> CacheTtlMillis {
200 CacheTtlMillis::new(value)
201 }
202
203 const fn timestamp(value: u64) -> CacheTimestampMillis {
204 CacheTimestampMillis::new(value)
205 }
206
207 const fn effective_ttl_ms(base_ttl_millis: u64, tier: CacheTier) -> u64 {
208 effective_ttl(ttl(base_ttl_millis), tier).get()
209 }
210
211 fn is_expired_ms(inserted_at_millis: u64, base_ttl_millis: u64, tier: CacheTier) -> bool {
212 is_expired(timestamp(inserted_at_millis), ttl(base_ttl_millis), tier)
213 }
214
215 #[test]
220 fn effective_ttl_tier_0_is_base() {
221 assert_eq!(effective_ttl_ms(1000, CacheTier::new(0)), 1000);
222 assert_eq!(effective_ttl_ms(0, CacheTier::new(0)), 0);
223 assert_eq!(effective_ttl_ms(1, CacheTier::new(0)), 1);
224 }
225
226 #[test]
227 fn effective_ttl_tier_1_is_2x() {
228 assert_eq!(effective_ttl_ms(1000, CacheTier::new(1)), 2000);
229 assert_eq!(effective_ttl_ms(500, CacheTier::new(1)), 1000);
230 }
231
232 #[test]
233 fn effective_ttl_tier_2_is_4x() {
234 assert_eq!(effective_ttl_ms(1000, CacheTier::new(2)), 4000);
235 }
236
237 #[test]
238 fn effective_ttl_tier_3_is_8x() {
239 assert_eq!(effective_ttl_ms(1000, CacheTier::new(3)), 8000);
240 }
241
242 #[test]
243 fn effective_ttl_tier_7_is_128x() {
244 assert_eq!(effective_ttl_ms(1000, CacheTier::new(7)), 128_000);
245 }
246
247 #[test]
248 fn effective_ttl_tier_10_is_1024x() {
249 assert_eq!(effective_ttl_ms(1000, CacheTier::new(10)), 1_024_000);
250 }
251
252 #[test]
253 fn effective_ttl_caps_at_tier_63() {
254 let tier_63_result = effective_ttl_ms(1, CacheTier::new(63));
255 assert_eq!(tier_63_result, 1u64 << 63);
256 assert_eq!(effective_ttl_ms(1, CacheTier::new(64)), tier_63_result);
258 assert_eq!(effective_ttl_ms(1, CacheTier::new(100)), tier_63_result);
259 assert_eq!(effective_ttl_ms(1, CacheTier::new(255)), tier_63_result);
260 }
261
262 #[test]
263 fn effective_ttl_saturates_on_overflow() {
264 assert_eq!(effective_ttl_ms(u64::MAX, CacheTier::new(0)), u64::MAX);
266 assert_eq!(effective_ttl_ms(u64::MAX, CacheTier::new(1)), u64::MAX);
267 assert_eq!(effective_ttl_ms(u64::MAX, CacheTier::new(63)), u64::MAX);
268
269 assert_eq!(
272 effective_ttl_ms(u64::MAX / 2 + 1, CacheTier::new(1)),
273 u64::MAX
274 );
275 }
276
277 #[test]
278 fn effective_ttl_zero_base() {
279 assert_eq!(effective_ttl_ms(0, CacheTier::new(0)), 0);
281 assert_eq!(effective_ttl_ms(0, CacheTier::new(1)), 0);
282 assert_eq!(effective_ttl_ms(0, CacheTier::new(63)), 0);
283 assert_eq!(effective_ttl_ms(0, CacheTier::new(255)), 0);
284 }
285
286 #[test]
291 fn ttl_multiplier_values() {
292 assert_eq!(ttl_multiplier(CacheTier::new(0)), 1);
293 assert_eq!(ttl_multiplier(CacheTier::new(1)), 2);
294 assert_eq!(ttl_multiplier(CacheTier::new(2)), 4);
295 assert_eq!(ttl_multiplier(CacheTier::new(3)), 8);
296 assert_eq!(ttl_multiplier(CacheTier::new(4)), 16);
297 assert_eq!(ttl_multiplier(CacheTier::new(5)), 32);
298 assert_eq!(ttl_multiplier(CacheTier::new(6)), 64);
299 assert_eq!(ttl_multiplier(CacheTier::new(7)), 128);
300 assert_eq!(ttl_multiplier(CacheTier::new(10)), 1024);
301 assert_eq!(ttl_multiplier(CacheTier::new(63)), 1u64 << 63);
302 }
303
304 #[test]
305 fn ttl_multiplier_caps_at_tier_63() {
306 let max = 1u64 << 63;
307 assert_eq!(ttl_multiplier(CacheTier::new(64)), max);
308 assert_eq!(ttl_multiplier(CacheTier::new(100)), max);
309 assert_eq!(ttl_multiplier(CacheTier::new(255)), max);
310 }
311
312 #[test]
317 fn is_expired_fresh_entry() {
318 let now = now_millis();
319 assert!(!is_expired_ms(now, 1000, CacheTier::new(0))); assert!(!is_expired_ms(now, 100, CacheTier::new(0))); }
322
323 #[test]
324 fn is_expired_old_entry() {
325 let old = now_millis().saturating_sub(2000);
326 assert!(is_expired_ms(old, 1000, CacheTier::new(0))); assert!(is_expired_ms(old, 1999, CacheTier::new(0))); }
329
330 #[test]
331 fn is_expired_boundary() {
332 let inserted = now_millis().saturating_sub(1000);
333 assert!(is_expired_ms(inserted, 1000, CacheTier::new(0)));
335 assert!(!is_expired_ms(inserted, 2000, CacheTier::new(0)));
337 }
338
339 #[test]
340 fn is_expired_respects_tier() {
341 let inserted = now_millis().saturating_sub(1500);
342 assert!(is_expired_ms(inserted, 1000, CacheTier::new(0))); assert!(!is_expired_ms(inserted, 1000, CacheTier::new(1))); assert!(!is_expired_ms(inserted, 1000, CacheTier::new(2))); }
347
348 #[test]
349 fn is_expired_high_tier_extends_ttl() {
350 let inserted = now_millis().saturating_sub(100_000); assert!(is_expired_ms(inserted, 1000, CacheTier::new(0))); assert!(is_expired_ms(inserted, 1000, CacheTier::new(1))); assert!(is_expired_ms(inserted, 1000, CacheTier::new(5))); assert!(is_expired_ms(inserted, 1000, CacheTier::new(6))); assert!(!is_expired_ms(inserted, 1000, CacheTier::new(7))); assert!(!is_expired_ms(inserted, 1000, CacheTier::new(10))); }
359
360 #[test]
361 fn is_expired_zero_ttl() {
362 let now = now_millis();
363 assert!(is_expired_ms(now, 0, CacheTier::new(0)));
365 assert!(is_expired_ms(now, 0, CacheTier::new(63))); }
367
368 #[test]
369 fn is_expired_future_timestamp() {
370 let future = now_millis().saturating_add(10000);
372 assert!(!is_expired_ms(future, 1000, CacheTier::new(0)));
374 }
375
376 #[test]
381 fn now_millis_is_reasonable() {
382 let now = now_millis();
383 assert!(now > 1_700_000_000_000);
386 }
387
388 #[test]
389 fn now_millis_is_monotonic() {
390 let t1 = now_millis();
391 let t2 = now_millis();
392 assert!(t2 >= t1);
393 }
394}