1use std::sync::Arc;
5use std::time::Duration;
6
7use crate::options::EntryOptions;
8use crate::tags::Tag;
9use crate::time::Timestamp;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Freshness {
18 Fresh,
20 Stale,
23}
24
25impl Freshness {
26 #[must_use]
28 pub fn is_fresh(self) -> bool {
29 matches!(self, Freshness::Fresh)
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct Metadata {
36 created: Timestamp,
38 logical_expiration: Timestamp,
40 physical_expiration: Timestamp,
42 backend_ttl: Duration,
46 is_from_fail_safe: bool,
49 eager_refresh_at: Option<Timestamp>,
51 etag: Option<String>,
53 last_modified: Option<Timestamp>,
55 tags: Box<[Tag]>,
57}
58
59impl Metadata {
60 #[must_use]
62 pub fn created(&self) -> Timestamp {
63 self.created
64 }
65
66 #[must_use]
68 pub fn logical_expiration(&self) -> Timestamp {
69 self.logical_expiration
70 }
71
72 #[must_use]
74 pub fn physical_expiration(&self) -> Timestamp {
75 self.physical_expiration
76 }
77
78 #[must_use]
80 pub fn tags(&self) -> &[Tag] {
81 &self.tags
82 }
83
84 #[must_use]
86 pub fn etag(&self) -> Option<&str> {
87 self.etag.as_deref()
88 }
89
90 #[must_use]
92 pub fn last_modified(&self) -> Option<Timestamp> {
93 self.last_modified
94 }
95
96 #[must_use]
98 pub fn is_from_fail_safe(&self) -> bool {
99 self.is_from_fail_safe
100 }
101}
102
103#[derive(Debug)]
109pub struct Entry<V> {
110 inner: Arc<EntryInner<V>>,
111}
112
113#[derive(Debug)]
114struct EntryInner<V> {
115 value: V,
116 meta: Metadata,
117}
118
119impl<V> Clone for Entry<V> {
120 fn clone(&self) -> Self {
121 Self {
122 inner: Arc::clone(&self.inner),
123 }
124 }
125}
126
127impl<V> Entry<V> {
128 #[must_use]
130 pub fn value(&self) -> &V {
131 &self.inner.value
132 }
133
134 #[must_use]
136 pub fn meta(&self) -> &Metadata {
137 &self.inner.meta
138 }
139
140 #[must_use]
142 pub fn backend_ttl(&self) -> Duration {
143 self.inner.meta.backend_ttl
144 }
145
146 #[must_use]
148 pub fn freshness(&self, now: Timestamp) -> Freshness {
149 if now.is_before(self.inner.meta.logical_expiration) {
150 Freshness::Fresh
151 } else {
152 Freshness::Stale
153 }
154 }
155
156 #[must_use]
158 pub fn is_logically_expired(&self, now: Timestamp) -> bool {
159 !now.is_before(self.inner.meta.logical_expiration)
160 }
161
162 #[must_use]
165 pub fn is_physically_expired(&self, now: Timestamp) -> bool {
166 !now.is_before(self.inner.meta.physical_expiration)
167 }
168
169 #[must_use]
171 pub fn should_eager_refresh(&self, now: Timestamp) -> bool {
172 if self.inner.meta.is_from_fail_safe {
173 return false;
174 }
175 match self.inner.meta.eager_refresh_at {
176 Some(at) => !now.is_before(at) && now.is_before(self.inner.meta.logical_expiration),
177 None => false,
178 }
179 }
180}
181
182impl<V: Clone> Entry<V> {
183 #[must_use]
185 pub fn value_cloned(&self) -> V {
186 self.inner.value.clone()
187 }
188}
189
190impl<V> Entry<V> {
191 #[must_use]
193 pub fn fresh(
194 value: V,
195 options: &EntryOptions,
196 created: Timestamp,
197 tags: Box<[Tag]>,
198 etag: Option<String>,
199 last_modified: Option<Timestamp>,
200 ) -> Self {
201 let physical_ttl = options.physical_ttl();
202 let physical_expiration = created.saturating_add(physical_ttl);
203 let meta = Metadata {
204 created,
205 logical_expiration: options.logical_expiration(created),
206 physical_expiration,
207 backend_ttl: physical_ttl,
208 is_from_fail_safe: false,
209 eager_refresh_at: options.eager_refresh_at(created),
210 etag,
211 last_modified,
212 tags,
213 };
214 Self {
215 inner: Arc::new(EntryInner { value, meta }),
216 }
217 }
218
219 #[must_use]
225 pub fn throttled(source: &Entry<V>, options: &EntryOptions, now: Timestamp) -> Option<Self>
226 where
227 V: Clone,
228 {
229 if source.is_physically_expired(now) {
230 return None;
231 }
232 let physical_expiration = source.inner.meta.physical_expiration;
233 let backend_ttl = physical_expiration.saturating_duration_since(now);
234 let logical_expiration = now.saturating_add(options.fail_safe_throttle_duration());
235 let meta = Metadata {
236 created: source.inner.meta.created,
237 logical_expiration,
238 physical_expiration,
239 backend_ttl,
240 is_from_fail_safe: true,
241 eager_refresh_at: None,
242 etag: source.inner.meta.etag.clone(),
243 last_modified: source.inner.meta.last_modified,
244 tags: source.inner.meta.tags.clone(),
245 };
246 Some(Self {
247 inner: Arc::new(EntryInner {
248 value: source.inner.value.clone(),
249 meta,
250 }),
251 })
252 }
253
254 #[must_use]
258 pub fn with_logical_expiration(&self, at: Timestamp) -> Self
259 where
260 V: Clone,
261 {
262 let mut meta = self.inner.meta.clone();
263 meta.logical_expiration = at;
264 meta.eager_refresh_at = None;
265 meta.backend_ttl = meta.physical_expiration.saturating_duration_since(at);
266 Self {
267 inner: Arc::new(EntryInner {
268 value: self.inner.value.clone(),
269 meta,
270 }),
271 }
272 }
273
274 #[allow(clippy::too_many_arguments)]
277 #[must_use]
278 pub fn rehydrate(
279 value: V,
280 created: Timestamp,
281 logical_expiration: Timestamp,
282 physical_expiration: Timestamp,
283 is_from_fail_safe: bool,
284 etag: Option<String>,
285 last_modified: Option<Timestamp>,
286 tags: Box<[Tag]>,
287 now: Timestamp,
288 ) -> Self {
289 let meta = Metadata {
290 created,
291 logical_expiration,
292 physical_expiration,
293 backend_ttl: physical_expiration.saturating_duration_since(now),
294 is_from_fail_safe,
295 eager_refresh_at: None,
296 etag,
297 last_modified,
298 tags,
299 };
300 Self {
301 inner: Arc::new(EntryInner { value, meta }),
302 }
303 }
304
305 #[must_use]
308 pub fn from_fail_safe_default(value: V, options: &EntryOptions, now: Timestamp) -> Self {
309 let throttle = options.fail_safe_throttle_duration();
310 let physical_expiration = now.saturating_add(options.physical_ttl());
311 let meta = Metadata {
312 created: now,
313 logical_expiration: now.saturating_add(throttle),
314 physical_expiration,
315 backend_ttl: options.physical_ttl(),
316 is_from_fail_safe: true,
317 eager_refresh_at: None,
318 etag: None,
319 last_modified: None,
320 tags: Box::from([]),
321 };
322 Self {
323 inner: Arc::new(EntryInner { value, meta }),
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use crate::time::Timestamp;
332
333 fn opts() -> EntryOptions {
334 EntryOptions::new(Duration::from_secs(10)).with_fail_safe(
335 true,
336 Some(Duration::from_secs(100)),
337 Some(Duration::from_secs(5)),
338 )
339 }
340
341 #[test]
342 fn fresh_entry_is_fresh_then_stale() {
343 let created = Timestamp::from_ticks(0);
344 let e = Entry::fresh(7i32, &opts(), created, Box::from([]), None, None);
345 let before = created.saturating_add(Duration::from_secs(5));
346 let after = created.saturating_add(Duration::from_secs(15));
347 assert_eq!(e.freshness(before), Freshness::Fresh);
348 assert_eq!(e.freshness(after), Freshness::Stale);
349 assert!(!e.is_physically_expired(after));
351 assert!(e.is_physically_expired(created.saturating_add(Duration::from_secs(101))));
352 }
353
354 #[test]
355 fn throttled_keeps_physical_boundary_and_resets_logical() {
356 let created = Timestamp::from_ticks(0);
357 let e = Entry::fresh(7i32, &opts(), created, Box::from([]), None, None);
358 let now = created.saturating_add(Duration::from_secs(20)); let t = Entry::throttled(&e, &opts(), now).expect("still physically alive");
360 assert!(t.meta().is_from_fail_safe());
361 assert_eq!(
363 t.freshness(now.saturating_add(Duration::from_secs(2))),
364 Freshness::Fresh
365 );
366 assert_eq!(
367 t.freshness(now.saturating_add(Duration::from_secs(6))),
368 Freshness::Stale
369 );
370 }
371
372 #[test]
373 fn throttled_none_when_physically_dead() {
374 let created = Timestamp::from_ticks(0);
375 let e = Entry::fresh(7i32, &opts(), created, Box::from([]), None, None);
376 let now = created.saturating_add(Duration::from_secs(200));
377 assert!(Entry::throttled(&e, &opts(), now).is_none());
378 }
379}