Compare commits
5 commits
0b762a36f3
...
9ba687bdae
Author | SHA1 | Date | |
---|---|---|---|
9ba687bdae | |||
4ac5223149 | |||
6a92c1713d | |||
b9f6d98d49 | |||
91b48850db |
8 changed files with 85 additions and 88 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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]]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
14
README.md
14
README.md
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
37
src/main.rs
37
src/main.rs
|
@ -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
|
||||||
|
|
|
@ -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()))?;
|
.await;
|
||||||
let mut entry = cache.first_entry();
|
|
||||||
while let Some(occupied) = entry {
|
let new_size = self.0.len();
|
||||||
map.serialize_entry(occupied.key(), occupied.get())?;
|
tracing::debug!("removed {i} entries ({old_size} -> {new_size} entries)");
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Cache::from_map(cache))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_map(CoolVisitor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue