use std::sync::Arc;
use std::time::Duration;
use crate::options::EntryOptions;
use crate::tags::Tag;
use crate::time::Timestamp;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Freshness {
Fresh,
Stale,
}
impl Freshness {
#[must_use]
pub fn is_fresh(self) -> bool {
matches!(self, Freshness::Fresh)
}
}
#[derive(Debug, Clone)]
pub struct Metadata {
created: Timestamp,
logical_expiration: Timestamp,
physical_expiration: Timestamp,
backend_ttl: Duration,
is_from_fail_safe: bool,
eager_refresh_at: Option<Timestamp>,
etag: Option<String>,
last_modified: Option<Timestamp>,
tags: Box<[Tag]>,
}
impl Metadata {
#[must_use]
pub fn created(&self) -> Timestamp {
self.created
}
#[must_use]
pub fn logical_expiration(&self) -> Timestamp {
self.logical_expiration
}
#[must_use]
pub fn physical_expiration(&self) -> Timestamp {
self.physical_expiration
}
#[must_use]
pub fn tags(&self) -> &[Tag] {
&self.tags
}
#[must_use]
pub fn etag(&self) -> Option<&str> {
self.etag.as_deref()
}
#[must_use]
pub fn last_modified(&self) -> Option<Timestamp> {
self.last_modified
}
#[must_use]
pub fn is_from_fail_safe(&self) -> bool {
self.is_from_fail_safe
}
}
#[derive(Debug)]
pub struct Entry<V> {
inner: Arc<EntryInner<V>>,
}
#[derive(Debug)]
struct EntryInner<V> {
value: V,
meta: Metadata,
}
impl<V> Clone for Entry<V> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<V> Entry<V> {
#[must_use]
pub fn value(&self) -> &V {
&self.inner.value
}
#[must_use]
pub fn meta(&self) -> &Metadata {
&self.inner.meta
}
#[must_use]
pub fn backend_ttl(&self) -> Duration {
self.inner.meta.backend_ttl
}
#[must_use]
pub fn freshness(&self, now: Timestamp) -> Freshness {
if now.is_before(self.inner.meta.logical_expiration) {
Freshness::Fresh
} else {
Freshness::Stale
}
}
#[must_use]
pub fn is_logically_expired(&self, now: Timestamp) -> bool {
!now.is_before(self.inner.meta.logical_expiration)
}
#[must_use]
pub fn is_physically_expired(&self, now: Timestamp) -> bool {
!now.is_before(self.inner.meta.physical_expiration)
}
#[must_use]
pub fn should_eager_refresh(&self, now: Timestamp) -> bool {
if self.inner.meta.is_from_fail_safe {
return false;
}
match self.inner.meta.eager_refresh_at {
Some(at) => !now.is_before(at) && now.is_before(self.inner.meta.logical_expiration),
None => false,
}
}
}
impl<V: Clone> Entry<V> {
#[must_use]
pub fn value_cloned(&self) -> V {
self.inner.value.clone()
}
}
impl<V> Entry<V> {
#[must_use]
pub fn fresh(
value: V,
options: &EntryOptions,
created: Timestamp,
tags: Box<[Tag]>,
etag: Option<String>,
last_modified: Option<Timestamp>,
) -> Self {
let physical_ttl = options.physical_ttl();
let physical_expiration = created.saturating_add(physical_ttl);
let meta = Metadata {
created,
logical_expiration: options.logical_expiration(created),
physical_expiration,
backend_ttl: physical_ttl,
is_from_fail_safe: false,
eager_refresh_at: options.eager_refresh_at(created),
etag,
last_modified,
tags,
};
Self {
inner: Arc::new(EntryInner { value, meta }),
}
}
#[must_use]
pub fn throttled(source: &Entry<V>, options: &EntryOptions, now: Timestamp) -> Option<Self>
where
V: Clone,
{
if source.is_physically_expired(now) {
return None;
}
let physical_expiration = source.inner.meta.physical_expiration;
let backend_ttl = physical_expiration.saturating_duration_since(now);
let logical_expiration = now.saturating_add(options.fail_safe_throttle_duration());
let meta = Metadata {
created: source.inner.meta.created,
logical_expiration,
physical_expiration,
backend_ttl,
is_from_fail_safe: true,
eager_refresh_at: None,
etag: source.inner.meta.etag.clone(),
last_modified: source.inner.meta.last_modified,
tags: source.inner.meta.tags.clone(),
};
Some(Self {
inner: Arc::new(EntryInner {
value: source.inner.value.clone(),
meta,
}),
})
}
#[must_use]
pub fn with_logical_expiration(&self, at: Timestamp) -> Self
where
V: Clone,
{
let mut meta = self.inner.meta.clone();
meta.logical_expiration = at;
meta.eager_refresh_at = None;
meta.backend_ttl = meta.physical_expiration.saturating_duration_since(at);
Self {
inner: Arc::new(EntryInner {
value: self.inner.value.clone(),
meta,
}),
}
}
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn rehydrate(
value: V,
created: Timestamp,
logical_expiration: Timestamp,
physical_expiration: Timestamp,
is_from_fail_safe: bool,
etag: Option<String>,
last_modified: Option<Timestamp>,
tags: Box<[Tag]>,
now: Timestamp,
) -> Self {
let meta = Metadata {
created,
logical_expiration,
physical_expiration,
backend_ttl: physical_expiration.saturating_duration_since(now),
is_from_fail_safe,
eager_refresh_at: None,
etag,
last_modified,
tags,
};
Self {
inner: Arc::new(EntryInner { value, meta }),
}
}
#[must_use]
pub fn from_fail_safe_default(value: V, options: &EntryOptions, now: Timestamp) -> Self {
let throttle = options.fail_safe_throttle_duration();
let physical_expiration = now.saturating_add(options.physical_ttl());
let meta = Metadata {
created: now,
logical_expiration: now.saturating_add(throttle),
physical_expiration,
backend_ttl: options.physical_ttl(),
is_from_fail_safe: true,
eager_refresh_at: None,
etag: None,
last_modified: None,
tags: Box::from([]),
};
Self {
inner: Arc::new(EntryInner { value, meta }),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::Timestamp;
fn opts() -> EntryOptions {
EntryOptions::new(Duration::from_secs(10)).with_fail_safe(
true,
Some(Duration::from_secs(100)),
Some(Duration::from_secs(5)),
)
}
#[test]
fn fresh_entry_is_fresh_then_stale() {
let created = Timestamp::from_ticks(0);
let e = Entry::fresh(7i32, &opts(), created, Box::from([]), None, None);
let before = created.saturating_add(Duration::from_secs(5));
let after = created.saturating_add(Duration::from_secs(15));
assert_eq!(e.freshness(before), Freshness::Fresh);
assert_eq!(e.freshness(after), Freshness::Stale);
assert!(!e.is_physically_expired(after));
assert!(e.is_physically_expired(created.saturating_add(Duration::from_secs(101))));
}
#[test]
fn throttled_keeps_physical_boundary_and_resets_logical() {
let created = Timestamp::from_ticks(0);
let e = Entry::fresh(7i32, &opts(), created, Box::from([]), None, None);
let now = created.saturating_add(Duration::from_secs(20)); let t = Entry::throttled(&e, &opts(), now).expect("still physically alive");
assert!(t.meta().is_from_fail_safe());
assert_eq!(
t.freshness(now.saturating_add(Duration::from_secs(2))),
Freshness::Fresh
);
assert_eq!(
t.freshness(now.saturating_add(Duration::from_secs(6))),
Freshness::Stale
);
}
#[test]
fn throttled_none_when_physically_dead() {
let created = Timestamp::from_ticks(0);
let e = Entry::fresh(7i32, &opts(), created, Box::from([]), None, None);
let now = created.saturating_add(Duration::from_secs(200));
assert!(Entry::throttled(&e, &opts(), now).is_none());
}
}