From 9b96b09ee0b96d25e978bc226c5b2610037867d0 Mon Sep 17 00:00:00 2001 From: slonkazoid Date: Sun, 29 Dec 2024 13:31:38 +0300 Subject: [PATCH] add cache ttl --- CONFIG.md | 6 ++- src/config.rs | 22 +++++++--- src/de.rs | 86 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 12 +++--- src/post/blag.rs | 4 +- src/post/cache.rs | 54 +++++++++++++++++------- src/post/markdown_posts.rs | 8 ++-- src/ranged_i128_visitor.rs | 37 ---------------- src/systemtime_as_secs.rs | 16 ++++--- 9 files changed, 168 insertions(+), 77 deletions(-) create mode 100644 src/de.rs delete mode 100644 src/ranged_i128_visitor.rs diff --git a/CONFIG.md b/CONFIG.md index c0b811e..e6ff3f0 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -43,8 +43,12 @@ port = 3000 # port to listen on [cache] enable = true # save metadata and rendered posts into RAM # highly recommended, only turn off if absolutely necessary +#ttl = 5 # how long should and item persist in cache, + # in milliseconds + # uncomment to enable cleanup = true # clean cache, highly recommended -#cleanup_interval = 86400000 # clean the cache regularly instead of just at startup +#cleanup_interval = 86400000 # clean the cache regularly instead of + # just at startup, value in milliseconds # uncomment to enable persistence = true # save the cache to on shutdown and load on startup file = "cache" # file to save the cache to diff --git a/src/config.rs b/src/config.rs index b1229f7..e1ad16c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,14 +1,15 @@ use std::env; use std::net::{IpAddr, Ipv6Addr}; +use std::num::NonZeroU64; use std::path::PathBuf; -use color_eyre::eyre::{bail, Context, Result}; +use color_eyre::eyre::{self, bail, Context}; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tracing::{error, info, instrument}; use url::Url; -use crate::ranged_i128_visitor::RangedI128Visitor; +use crate::de::*; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] #[serde(default)] @@ -31,8 +32,11 @@ pub struct RenderConfig { #[serde(default)] pub struct CacheConfig { pub enable: bool, + #[serde(deserialize_with = "check_millis")] + pub ttl: Option, pub cleanup: bool, - pub cleanup_interval: Option, + #[serde(deserialize_with = "check_millis")] + pub cleanup_interval: Option, pub persistence: bool, pub file: PathBuf, pub compress: bool, @@ -198,6 +202,7 @@ impl Default for CacheConfig { fn default() -> Self { Self { enable: true, + ttl: None, cleanup: true, cleanup_interval: None, persistence: true, @@ -215,7 +220,7 @@ impl Default for BlagConfig { } #[instrument(name = "config")] -pub async fn load() -> Result { +pub async fn load() -> eyre::Result { let config_file = env::var(format!( "{}_CONFIG", env!("CARGO_BIN_NAME").to_uppercase().replace('-', "_") @@ -267,6 +272,13 @@ fn check_zstd_level_bounds<'de, D>(d: D) -> Result where D: serde::Deserializer<'de>, { - d.deserialize_i32(RangedI128Visitor::<1, 22>) + d.deserialize_i32(RangedI64Visitor::<1, 22>) .map(|x| x as i32) } + +fn check_millis<'de, D>(d: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + d.deserialize_option(MillisVisitor) +} diff --git a/src/de.rs b/src/de.rs new file mode 100644 index 0000000..0afc59f --- /dev/null +++ b/src/de.rs @@ -0,0 +1,86 @@ +use std::num::NonZeroU64; + +use serde::de::Error; + +use serde::{ + de::{Unexpected, Visitor}, + Deserializer, +}; + +pub struct RangedI64Visitor; +impl serde::de::Visitor<'_> for RangedI64Visitor { + type Value = i64; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "an integer between {START} and {END}") + } + + fn visit_i32(self, v: i32) -> Result + where + E: serde::de::Error, + { + self.visit_i64(v as i64) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + if v >= START && v <= END { + Ok(v) + } else { + Err(E::custom(format!( + "integer is out of bounds ({START}..{END})" + ))) + } + } + + fn visit_i128(self, v: i128) -> Result + where + E: serde::de::Error, + { + self.visit_i64(v as i64) + } +} + +pub struct U64Visitor; +impl Visitor<'_> for U64Visitor { + type Value = u64; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a non-negative integer") + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + Ok(v) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + u64::try_from(v).map_err(|_| E::invalid_value(Unexpected::Signed(v), &self)) + } +} + +pub struct MillisVisitor; +impl<'de> Visitor<'de> for MillisVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a positive integer") + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let n = deserializer.deserialize_i64(U64Visitor)?; + NonZeroU64::new(n) + .ok_or(D::Error::invalid_value(Unexpected::Unsigned(n), &self)) + .map(Some) + } +} diff --git a/src/main.rs b/src/main.rs index c357b81..8d5f2a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,12 @@ mod app; mod config; +mod de; mod error; mod helpers; mod markdown_render; mod platform; mod post; -mod ranged_i128_visitor; mod serve_dir_included; mod systemtime_as_secs; mod templates; @@ -32,7 +32,7 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::{util::SubscriberInitExt, EnvFilter}; use crate::app::AppState; -use crate::post::cache::{load_cache, CacheGuard, CACHE_VERSION}; +use crate::post::cache::{load_cache, Cache, CacheGuard, CACHE_VERSION}; use crate::post::{Blag, MarkdownPosts, PostManager}; use crate::templates::new_registry; use crate::templates::watcher::watch_templates; @@ -89,17 +89,17 @@ async fn main() -> eyre::Result<()> { let mut cache = load_cache(&config.cache).await.unwrap_or_else(|err| { error!("failed to load cache: {}", err); info!("using empty cache"); - Default::default() + Cache::new(config.cache.ttl) }); if cache.version() < CACHE_VERSION { warn!("cache version changed, clearing cache"); - cache = Default::default(); + cache = Cache::new(config.cache.ttl); }; Some(cache) } else { - Some(Default::default()) + Some(Cache::new(config.cache.ttl)) } } else { None @@ -122,7 +122,7 @@ async fn main() -> eyre::Result<()> { let token = cancellation_token.child_token(); debug!("setting up cleanup task"); tasks.spawn(async move { - let mut interval = tokio::time::interval(Duration::from_millis(millis)); + let mut interval = tokio::time::interval(Duration::from_millis(millis.into())); loop { select! { _ = token.cancelled() => break Ok(()), diff --git a/src/post/blag.rs b/src/post/blag.rs index c9c5379..b4b12c4 100644 --- a/src/post/blag.rs +++ b/src/post/blag.rs @@ -248,7 +248,7 @@ impl PostManager for Blag { return Err(PostError::NotFound(name)); } - let mtime = as_secs(&stat.modified()?); + let mtime = as_secs(stat.modified()?); let query_json = serde_json::to_string(&query).expect("this should not fail"); let mut hasher = DefaultHasher::new(); @@ -314,7 +314,7 @@ impl PostManager for Blag { ) .ok() .and_then(|metadata| metadata.modified().ok()) - .map(|mtime| as_secs(&mtime)); + .map(as_secs); match mtime { Some(mtime) => mtime <= value.mtime, diff --git a/src/post/cache.rs b/src/post/cache.rs index 4c17c46..0fc8c3d 100644 --- a/src/post/cache.rs +++ b/src/post/cache.rs @@ -1,7 +1,9 @@ use std::fmt::Debug; use std::io::{Read, Write}; +use std::num::NonZeroU64; use std::ops::Deref; use std::sync::Arc; +use std::time::SystemTime; use crate::config::CacheConfig; use crate::post::PostMetadata; @@ -14,20 +16,24 @@ use tracing::{debug, info, instrument, trace, Span}; /// do not persist cache if this version number changed pub const CACHE_VERSION: u16 = 5; +fn now() -> u128 { + crate::systemtime_as_secs::as_millis(SystemTime::now()) +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct CacheValue { pub meta: PostMetadata, pub body: Arc, pub mtime: u64, + /// when the item was inserted into cache, in milliseconds since epoch + pub cached_at: u128, } #[derive(Serialize, Deserialize, Clone)] -pub struct Cache(HashMap, u16); - -impl Default for Cache { - fn default() -> Self { - Self(Default::default(), CACHE_VERSION) - } +pub struct Cache { + map: HashMap, + version: u16, + ttl: Option, } #[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Clone, Debug)] @@ -38,15 +44,30 @@ pub struct CacheKey { } impl Cache { + pub fn new(ttl: Option) -> Self { + Cache { + map: Default::default(), + version: CACHE_VERSION, + ttl, + } + } + + fn up_to_date(&self, cached: &CacheValue, mtime: u64) -> bool { + mtime <= cached.mtime + && self + .ttl + .is_some_and(|ttl| cached.cached_at + u64::from(ttl) as u128 >= now()) + } + #[instrument(level = "debug", skip(self), fields(entry_mtime))] pub async fn lookup(&self, name: Arc, mtime: u64, extra: u64) -> Option { trace!("looking up in cache"); - match self.0.get_async(&CacheKey { name, extra }).await { + match self.map.get_async(&CacheKey { name, extra }).await { Some(entry) => { let cached = entry.get(); Span::current().record("entry_mtime", cached.mtime); trace!("found in cache"); - if mtime <= cached.mtime { + if self.up_to_date(cached, mtime) { trace!("entry up-to-date"); Some(cached.clone()) } else { @@ -67,11 +88,11 @@ impl Cache { extra: u64, ) -> Option { trace!("looking up metadata in cache"); - match self.0.get_async(&CacheKey { name, extra }).await { + match self.map.get_async(&CacheKey { name, extra }).await { Some(entry) => { let cached = entry.get(); Span::current().record("entry_mtime", cached.mtime); - if mtime <= cached.mtime { + if self.up_to_date(cached, mtime) { trace!("entry up-to-date"); Some(cached.meta.clone()) } else { @@ -96,13 +117,14 @@ impl Cache { trace!("inserting into cache"); let r = self - .0 + .map .upsert_async( CacheKey { name, extra }, CacheValue { meta: metadata, body: rendered, mtime, + cached_at: now(), }, ) .await; @@ -123,7 +145,7 @@ impl Cache { pub async fn remove(&self, name: Arc, extra: u64) -> Option<(CacheKey, CacheValue)> { trace!("removing from cache"); - let r = self.0.remove_async(&CacheKey { name, extra }).await; + let r = self.map.remove_async(&CacheKey { name, extra }).await; debug!( "item {} cache", @@ -138,12 +160,12 @@ impl Cache { #[instrument(level = "debug", name = "cleanup", skip_all)] pub async fn retain(&self, predicate: impl Fn(&CacheKey, &CacheValue) -> bool) { - let old_size = self.0.len(); + let old_size = self.map.len(); let mut i = 0; // TODO: multithread // not urgent as this is run concurrently anyways - self.0 + self.map .retain_async(|k, v| { if predicate(k, v) { true @@ -160,12 +182,12 @@ impl Cache { } pub fn len(&self) -> usize { - self.0.len() + self.map.len() } #[inline(always)] pub fn version(&self) -> u16 { - self.1 + self.version } } diff --git a/src/post/markdown_posts.rs b/src/post/markdown_posts.rs index 65ca241..df4141c 100644 --- a/src/post/markdown_posts.rs +++ b/src/post/markdown_posts.rs @@ -125,7 +125,7 @@ impl MarkdownPosts { .insert( name.clone(), metadata.clone(), - as_secs(&modified), + as_secs(modified), Arc::clone(&post), self.render_hash, ) @@ -184,7 +184,7 @@ impl PostManager for MarkdownPosts { let stat = fs::metadata(&path).await?; if stat.is_file() && path.extension().is_some_and(|ext| ext == "md") { - let mtime = as_secs(&stat.modified()?); + let mtime = as_secs(stat.modified()?); let name: Arc = String::from(path.file_stem().unwrap().to_string_lossy()).into(); @@ -262,7 +262,7 @@ impl PostManager for MarkdownPosts { } } }; - let mtime = as_secs(&stat.modified()?); + let mtime = as_secs(stat.modified()?); if let Some(cache) = &self.cache && let Some(CacheValue { meta, body, .. }) = @@ -311,7 +311,7 @@ impl PostManager for MarkdownPosts { ) .ok() .and_then(|metadata| metadata.modified().ok()) - .map(|mtime| as_secs(&mtime)); + .map(as_secs); match mtime { Some(mtime) => mtime <= value.mtime, diff --git a/src/ranged_i128_visitor.rs b/src/ranged_i128_visitor.rs deleted file mode 100644 index 8436fa4..0000000 --- a/src/ranged_i128_visitor.rs +++ /dev/null @@ -1,37 +0,0 @@ -pub struct RangedI128Visitor; -impl serde::de::Visitor<'_> - for RangedI128Visitor -{ - type Value = i128; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(formatter, "an integer between {START} and {END}") - } - - fn visit_i32(self, v: i32) -> std::result::Result - where - E: serde::de::Error, - { - self.visit_i128(v as i128) - } - - fn visit_i64(self, v: i64) -> std::prelude::v1::Result - where - E: serde::de::Error, - { - self.visit_i128(v as i128) - } - - fn visit_i128(self, v: i128) -> std::prelude::v1::Result - where - E: serde::de::Error, - { - if v >= START && v <= END { - Ok(v) - } else { - Err(E::custom(format!( - "integer is out of bounds ({START}..{END})" - ))) - } - } -} diff --git a/src/systemtime_as_secs.rs b/src/systemtime_as_secs.rs index 5e8e913..ce845d1 100644 --- a/src/systemtime_as_secs.rs +++ b/src/systemtime_as_secs.rs @@ -1,9 +1,13 @@ use std::time::SystemTime; -pub fn as_secs(t: &SystemTime) -> u64 { - match t.duration_since(SystemTime::UNIX_EPOCH) { - Ok(duration) => duration, - Err(err) => err.duration(), - } - .as_secs() +pub fn as_secs(t: SystemTime) -> u64 { + t.duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_else(|err| err.duration()) + .as_secs() +} + +pub fn as_millis(t: SystemTime) -> u128 { + t.duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_else(|err| err.duration()) + .as_millis() }