add cache ttl
This commit is contained in:
parent
74b61e67f5
commit
9b96b09ee0
9 changed files with 168 additions and 77 deletions
|
@ -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
|
||||
|
|
|
@ -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<NonZeroU64>,
|
||||
pub cleanup: bool,
|
||||
pub cleanup_interval: Option<u64>,
|
||||
#[serde(deserialize_with = "check_millis")]
|
||||
pub cleanup_interval: Option<NonZeroU64>,
|
||||
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<Config> {
|
||||
pub async fn load() -> eyre::Result<Config> {
|
||||
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<i32, D::Error>
|
|||
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<Option<NonZeroU64>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
d.deserialize_option(MillisVisitor)
|
||||
}
|
||||
|
|
86
src/de.rs
Normal file
86
src/de.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
use std::num::NonZeroU64;
|
||||
|
||||
use serde::de::Error;
|
||||
|
||||
use serde::{
|
||||
de::{Unexpected, Visitor},
|
||||
Deserializer,
|
||||
};
|
||||
|
||||
pub struct RangedI64Visitor<const START: i64, const END: i64>;
|
||||
impl<const START: i64, const END: i64> serde::de::Visitor<'_> for RangedI64Visitor<START, END> {
|
||||
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<E>(self, v: i32) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
self.visit_i64(v as i64)
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||
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<E>(self, v: i128) -> Result<Self::Value, E>
|
||||
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<E>(self, v: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||
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<NonZeroU64>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "a positive integer")
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
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)
|
||||
}
|
||||
}
|
12
src/main.rs
12
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(()),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<str>,
|
||||
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<CacheKey, CacheValue>, u16);
|
||||
|
||||
impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Self(Default::default(), CACHE_VERSION)
|
||||
}
|
||||
pub struct Cache {
|
||||
map: HashMap<CacheKey, CacheValue>,
|
||||
version: u16,
|
||||
ttl: Option<NonZeroU64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Clone, Debug)]
|
||||
|
@ -38,15 +44,30 @@ pub struct CacheKey {
|
|||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn new(ttl: Option<NonZeroU64>) -> 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<str>, mtime: u64, extra: u64) -> Option<CacheValue> {
|
||||
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<PostMetadata> {
|
||||
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<str>, 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<str> =
|
||||
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,
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
pub struct RangedI128Visitor<const START: i128, const END: i128>;
|
||||
impl<const START: i128, const END: i128> serde::de::Visitor<'_>
|
||||
for RangedI128Visitor<START, END>
|
||||
{
|
||||
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<E>(self, v: i32) -> std::result::Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
self.visit_i128(v as i128)
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, v: i64) -> std::prelude::v1::Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
self.visit_i128(v as i128)
|
||||
}
|
||||
|
||||
fn visit_i128<E>(self, v: i128) -> std::prelude::v1::Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
if v >= START && v <= END {
|
||||
Ok(v)
|
||||
} else {
|
||||
Err(E::custom(format!(
|
||||
"integer is out of bounds ({START}..{END})"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue