Compare commits

...

5 commits

8 changed files with 85 additions and 88 deletions

1
Cargo.lock generated
View file

@ -1748,6 +1748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec96560eea317a9cc4e0bb1f6a2c93c09a19b8c4fc5cb3fcc0ec1c094cd783e2" checksum = "ec96560eea317a9cc4e0bb1f6a2c93c09a19b8c4fc5cb3fcc0ec1c094cd783e2"
dependencies = [ dependencies = [
"sdd", "sdd",
"serde",
] ]
[[package]] [[package]]

View file

@ -25,7 +25,7 @@ comrak = { version = "0.22.0", features = ["syntect"] }
console-subscriber = { version = "0.2.0", optional = true } console-subscriber = { version = "0.2.0", optional = true }
fronma = "0.2.0" fronma = "0.2.0"
notify = "6.1.1" notify = "6.1.1"
scc = "2.1.0" scc = { version = "2.1.0", features = ["serde"] }
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
syntect = "5.2.0" syntect = "5.2.0"
thiserror = "1.0.58" thiserror = "1.0.58"

View file

@ -18,7 +18,7 @@ blazingly fast markdown blog software written in rust memory safe
- [ ] general cleanup of code - [ ] general cleanup of code
- [ ] better error reporting and error pages - [ ] better error reporting and error pages
- [ ] better tracing - [ ] better tracing
- [ ] cache cleanup task - [x] cache cleanup task
- [ ] ^ replace HashMap with HashCache once i implement [this](https://github.com/wvwwvwwv/scalable-concurrent-containers/issues/139) - [ ] ^ replace HashMap with HashCache once i implement [this](https://github.com/wvwwvwwv/scalable-concurrent-containers/issues/139)
- [x] (de)compress cache with zstd on startup/shutdown - [x] (de)compress cache with zstd on startup/shutdown
- [ ] make date parsing less strict - [ ] make date parsing less strict
@ -45,7 +45,10 @@ markdown_access = true # allow users to see the raw markdown of a post
[cache] # cache settings [cache] # cache settings
enable = true # save metadata and rendered posts into RAM enable = true # save metadata and rendered posts into RAM
# highly recommended, only turn off if absolutely necessary # highly recommended, only turn off if absolutely necessary
persistence = false # save the cache to on shutdown and load on startup cleanup = true # clean cache, highly recommended
#cleanup_interval = 86400000 # clean the cache regularly instead of just at startu
# uncomment to enable
persistence = true # save the cache to on shutdown and load on startup
file = "cache" # file to save the cache to file = "cache" # file to save the cache to
compress = true # compress the cache file compress = true # compress the cache file
compression_level = 3 # zstd compression level, 3 is recommended compression_level = 3 # zstd compression level, 3 is recommended
@ -60,10 +63,13 @@ you don't have to copy it from here, it's generated if it doesn't exist
## Usage ## Usage
this project uses nightly-only features.
make sure you have the nightly toolchain installed.
build the application with `cargo`: build the application with `cargo`:
```sh ```sh
cargo build --release cargo +nightly build --release
``` ```
the executable will be located at `target/release/bingus-blog`. the executable will be located at `target/release/bingus-blog`.
@ -79,7 +85,7 @@ building for `aarch64-unknown-linux-musl` (for example, a Redmi 5 Plus running p
sudo pacman -S aarch64-linux-gnu-gcc sudo pacman -S aarch64-linux-gnu-gcc
export CC=aarch64-linux-gnu-gcc export CC=aarch64-linux-gnu-gcc
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=$CC export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=$CC
cargo build --release --target=aarch64-unknown-linux-musl cargo +nightly build --release --target=aarch64-unknown-linux-musl
``` ```
your executable will be located at `target/<target>/release/bingus-blog` this time. your executable will be located at `target/<target>/release/bingus-blog` this time.

View file

@ -29,6 +29,8 @@ pub struct RenderConfig {
#[serde(default)] #[serde(default)]
pub struct CacheConfig { pub struct CacheConfig {
pub enable: bool, pub enable: bool,
pub cleanup: bool,
pub cleanup_interval: Option<u64>,
pub persistence: bool, pub persistence: bool,
pub file: PathBuf, pub file: PathBuf,
pub compress: bool, pub compress: bool,
@ -78,7 +80,9 @@ impl Default for CacheConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
enable: true, enable: true,
persistence: false, cleanup: true,
cleanup_interval: None,
persistence: true,
file: "cache".into(), file: "cache".into(),
compress: true, compress: true,
compression_level: 3, compression_level: 3,

View file

@ -7,6 +7,7 @@ mod hash_arc_store;
mod markdown_render; mod markdown_render;
mod post; mod post;
mod ranged_i128_visitor; mod ranged_i128_visitor;
mod systemtime_as_secs;
use std::future::IntoFuture; use std::future::IntoFuture;
use std::io::Read; use std::io::Read;
@ -25,13 +26,13 @@ use color_eyre::eyre::{self, Context};
use thiserror::Error; use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::signal;
use tokio::task::JoinSet; use tokio::task::JoinSet;
use tokio::{select, signal};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use tracing::{error, info, info_span, warn, Span}; use tracing::{debug, error, info, info_span, warn, Span};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use crate::config::Config; use crate::config::Config;
@ -160,7 +161,7 @@ async fn main() -> eyre::Result<()> {
.context("couldn't load configuration")?; .context("couldn't load configuration")?;
let mut tasks = JoinSet::new(); let mut tasks = JoinSet::new();
let mut cancellation_tokens = Vec::new(); let cancellation_token = CancellationToken::new();
let posts = if config.cache.enable { let posts = if config.cache.enable {
if config.cache.persistence if config.cache.persistence
@ -228,6 +229,27 @@ async fn main() -> eyre::Result<()> {
let state = Arc::new(AppState { config, posts }); let state = Arc::new(AppState { config, posts });
if state.config.cache.enable && state.config.cache.cleanup {
if let Some(t) = state.config.cache.cleanup_interval {
let state = Arc::clone(&state);
let token = cancellation_token.child_token();
debug!("setting up cleanup task");
tasks.spawn(async move {
let mut interval = tokio::time::interval(Duration::from_millis(t));
loop {
select! {
_ = token.cancelled() => break,
_ = interval.tick() => {
state.posts.cleanup().await
}
}
}
});
} else {
state.posts.cleanup().await;
}
}
let app = Router::new() let app = Router::new()
.route("/", get(index)) .route("/", get(index))
.route( .route(
@ -285,8 +307,7 @@ async fn main() -> eyre::Result<()> {
#[cfg(not(unix))] // TODO: kill all windows server users #[cfg(not(unix))] // TODO: kill all windows server users
let sigterm = std::future::pending::<()>(); let sigterm = std::future::pending::<()>();
let axum_token = CancellationToken::new(); let axum_token = cancellation_token.child_token();
cancellation_tokens.push(axum_token.clone());
let mut server = axum::serve( let mut server = axum::serve(
listener, listener,
@ -309,9 +330,7 @@ async fn main() -> eyre::Result<()> {
let cleanup = async move { let cleanup = async move {
// stop tasks // stop tasks
for token in cancellation_tokens { cancellation_token.cancel();
token.cancel();
}
server.await.context("failed to serve app")?; server.await.context("failed to serve app")?;
while let Some(task) = tasks.join_next().await { while let Some(task) = tasks.join_next().await {
task.context("failed to join task")?; task.context("failed to join task")?;
@ -320,6 +339,8 @@ async fn main() -> eyre::Result<()> {
// write cache to file // write cache to file
let AppState { config, posts } = Arc::<AppState>::try_unwrap(state).unwrap_or_else(|state| { let AppState { config, posts } = Arc::<AppState>::try_unwrap(state).unwrap_or_else(|state| {
warn!("couldn't unwrap Arc over AppState, more than one strong reference exists for Arc. cloning instead"); warn!("couldn't unwrap Arc over AppState, more than one strong reference exists for Arc. cloning instead");
// TODO: only do this when persistence is enabled
// first check config from inside the arc, then try unwrap
AppState::clone(state.as_ref()) AppState::clone(state.as_ref())
}); });
if config.cache.enable if config.cache.enable

View file

@ -1,9 +1,8 @@
use std::hash::{DefaultHasher, Hash, Hasher}; use std::hash::{DefaultHasher, Hash, Hasher};
use scc::HashMap; use scc::HashMap;
use serde::de::Visitor; use serde::{Deserialize, Serialize};
use serde::ser::SerializeMap; use tracing::instrument;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::config::RenderConfig; use crate::config::RenderConfig;
use crate::post::PostMetadata; use crate::post::PostMetadata;
@ -16,14 +15,10 @@ pub struct CacheValue {
config_hash: u64, config_hash: u64,
} }
#[derive(Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct Cache(HashMap<String, CacheValue>); pub struct Cache(HashMap<String, CacheValue>);
impl Cache { impl Cache {
pub fn from_map(cache: HashMap<String, CacheValue>) -> Self {
Self(cache)
}
pub async fn lookup( pub async fn lookup(
&self, &self,
name: &str, name: &str,
@ -102,58 +97,24 @@ impl Cache {
self.0.remove_async(name).await self.0.remove_async(name).await
} }
#[inline(always)] #[instrument(name = "cleanup", skip_all)]
pub fn into_inner(self) -> HashMap<String, CacheValue> { pub async fn cleanup(&self, get_mtime: impl Fn(&str) -> Option<u64>) {
let old_size = self.0.len();
let mut i = 0;
self.0 self.0
} .retain_async(|k, v| {
} if get_mtime(k).is_some_and(|mtime| mtime == v.mtime) {
true
impl Serialize for Cache { } else {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> tracing::debug!("removing {k} from cache");
where i += 1;
S: Serializer, false
{
let cache = self.clone().into_inner();
let mut map = serializer.serialize_map(Some(cache.len()))?;
let mut entry = cache.first_entry();
while let Some(occupied) = entry {
map.serialize_entry(occupied.key(), occupied.get())?;
entry = occupied.next();
}
map.end()
}
}
impl<'de> Deserialize<'de> for Cache {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct CoolVisitor;
impl<'de> Visitor<'de> for CoolVisitor {
type Value = Cache;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "expected a map")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let cache = match map.size_hint() {
Some(size) => HashMap::with_capacity(size),
None => HashMap::new(),
};
while let Some((key, value)) = map.next_entry::<String, CacheValue>()? {
cache.insert(key, value).ok();
} }
})
.await;
Ok(Cache::from_map(cache)) let new_size = self.0.len();
} tracing::debug!("removed {i} entries ({old_size} -> {new_size} entries)");
}
deserializer.deserialize_map(CoolVisitor)
} }
} }

View file

@ -15,6 +15,7 @@ use tracing::warn;
use crate::config::RenderConfig; use crate::config::RenderConfig;
use crate::markdown_render::render; use crate::markdown_render::render;
use crate::post::cache::Cache; use crate::post::cache::Cache;
use crate::systemtime_as_secs::as_secs;
use crate::PostError; use crate::PostError;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -134,10 +135,7 @@ impl PostManager {
.insert( .insert(
name.to_string(), name.to_string(),
metadata.clone(), metadata.clone(),
modified as_secs(&modified),
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs(),
post.clone(), post.clone(),
&self.config, &self.config,
) )
@ -157,11 +155,8 @@ impl PostManager {
let stat = fs::metadata(&path).await?; let stat = fs::metadata(&path).await?;
if stat.is_file() && path.extension().is_some_and(|ext| ext == "md") { if stat.is_file() && path.extension().is_some_and(|ext| ext == "md") {
let mtime = stat let mtime = as_secs(&stat.modified()?);
.modified()? // TODO. this?
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let name = path let name = path
.clone() .clone()
.file_stem() .file_stem()
@ -202,11 +197,7 @@ impl PostManager {
_ => return Err(PostError::IoError(err)), _ => return Err(PostError::IoError(err)),
}, },
}; };
let mtime = stat let mtime = as_secs(&stat.modified()?);
.modified()?
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
if let Some(cache) = self.cache.as_ref() if let Some(cache) = self.cache.as_ref()
&& let Some(hit) = cache.lookup(name, mtime, &self.config).await && let Some(hit) = cache.lookup(name, mtime, &self.config).await
@ -229,4 +220,17 @@ impl PostManager {
pub fn into_cache(self) -> Option<Cache> { pub fn into_cache(self) -> Option<Cache> {
self.cache self.cache
} }
pub async fn cleanup(&self) {
if let Some(cache) = self.cache.as_ref() {
cache
.cleanup(|name| {
std::fs::metadata(self.dir.join(name.to_owned() + ".md"))
.ok()
.and_then(|metadata| metadata.modified().ok())
.map(|mtime| as_secs(&mtime))
})
.await
}
}
} }

View file

@ -26,8 +26,8 @@ pre > code {
padding: 1.25em 1.5em; padding: 1.25em 1.5em;
display: block; display: block;
background-color: var(--base); background-color: unset;
color: var(--text); color: unset;
} }
img { img {