Compare commits

..
Author SHA1 Message Date
789ba410f7 Concurrent cache 2026-07-25 20:27:47 +02:00
39ebfcb1db Update deps 2026-07-25 20:12:48 +02:00
6 changed files with 785 additions and 673 deletions

1383
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,31 +5,33 @@ edition = "2024"
[dependencies] [dependencies]
# CLI options # CLI options
argp = "0.4.0" argp = "0.4.1"
# Templates # Templates
askama = "0.12.1" askama = "0.16.0"
async-lock = "3.4.2" async-lock = "3.4.2"
base64-turbo = "0.1.3" base64-turbo = "0.2.0"
# TOML parser # TOML parser
boml = "1.0.2" boml = "2.0.0"
# Concurrent hash map for caching
dashmap = "6.2.1"
#flate2 = "1.1.8" #flate2 = "1.1.8"
# request parsing # request parsing
form_urlencoded = "1.2.2" form_urlencoded = "1.2.2"
# Code highlight # Code highlight
giallo = { version = "0.3.1", features = ["dump"] } giallo = { version = "0.5.0", features = ["dump"] }
log = "0.4.29" log = "0.4.33"
rand = "0.9.2" rand = "0.10.2"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }
sha2 = "0.10.9" sha2 = "0.11.0"
simplelog = "0.12.2" simplelog = "0.12.2"
#tar = "0.4.44" #tar = "0.4.44"
# Web server # Web server
trillium = "0.2.20" trillium = "1.3.0"
trillium-askama = "0.3.2" trillium-askama = "0.5.0"
trillium-caching-headers = "0.2.3" trillium-caching-headers = "0.4.2"
trillium-client = { version = "0.6.2", features = ["json"] } trillium-client = { version = "0.9.13", features = ["serde_json"] }
trillium-native-tls = "0.4.0" trillium-native-tls = "0.6.3"
trillium-router = "0.4.1" trillium-router = "0.5.2"
trillium-smol = "0.4.2" trillium-smol = "0.7.0"
#trillium-static-compiled = "0.5.2" #trillium-static-compiled = "0.5.2"
url = "2.5.8" url = "2.5.8"

View file

@ -5,7 +5,7 @@ use crate::{
repo::{RepoMetadata, WriteRepoMetadataError}, repo::{RepoMetadata, WriteRepoMetadataError},
}; };
use rand::Rng; use rand::RngExt;
use serde::Deserialize; use serde::Deserialize;
use sha2::Digest; use sha2::Digest;
use trillium_client::Client; use trillium_client::Client;

View file

@ -1,47 +1,50 @@
use dashmap::DashMap;
use std::{ use std::{
borrow::Borrow, borrow::Borrow,
collections::{HashMap, VecDeque},
hash::Hash, hash::Hash,
time::Instant, sync::atomic::{AtomicU64, Ordering::Relaxed},
}; };
pub struct CacheEntry<T> { pub struct CacheEntry<T> {
inner: T, inner: T,
time: u64, time: AtomicU64,
} }
#[derive(Default)] #[derive(Default)]
pub struct Cache<K, V> { pub struct Cache<K: Eq + Hash, V> {
ttl: u64, ttl: u64,
size: usize, size: usize,
content: HashMap<K, CacheEntry<V>>, content: DashMap<K, CacheEntry<V>>,
} }
impl<K: Clone + Eq + Hash, V: Clone> Cache<K, V> { impl<K: Clone + Eq + Hash, V: Clone> Cache<K, V> {
pub fn sweep(&mut self) { pub fn sweep(&self) {
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
self.content.retain(|_k, entry| entry.time + self.ttl < now); self.content
.retain(|_k, entry| entry.time.load(Relaxed) + self.ttl < now);
} }
fn sweep_oldest(&mut self) { fn sweep_oldest(&self) {
// TODO sweep the N oldest // TODO sweep the N oldest
let mut oldest = None; let mut oldest = None;
let mut time = u64::MAX; let mut time = u64::MAX;
for (key, entry) in self.content.iter() { for item in self.content.iter() {
if entry.time < time { let (key, entry) = item.pair();
time = entry.time; let entry_time = entry.time.load(Relaxed);
oldest = Some(key); if entry_time < time {
time = entry_time;
oldest = Some(key.clone());
} }
} }
if let Some(oldest) = oldest.cloned() { if let Some(oldest) = oldest {
self.content.remove(&oldest); self.content.remove(&oldest);
} }
} }
pub fn fetch(&mut self, key: impl Borrow<K>, f: impl Fn(K) -> Option<V>) -> Option<V> { pub fn fetch(&self, key: impl Borrow<K>, f: impl Fn(K) -> Option<V>) -> Option<V> {
if let Some(entry) = self.content.get_mut(key.borrow()) { if let Some(entry) = self.content.get(key.borrow()) {
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
entry.time = now; entry.time.store(now, Relaxed);
Some(entry.inner.clone()) Some(entry.inner.clone())
} else if let Some(inner) = (f)(key.borrow().clone()) { } else if let Some(inner) = (f)(key.borrow().clone()) {
if self.content.len() >= self.size { if self.content.len() >= self.size {
@ -49,7 +52,7 @@ impl<K: Clone + Eq + Hash, V: Clone> Cache<K, V> {
} }
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let entry = CacheEntry { let entry = CacheEntry {
time: now, time: AtomicU64::new(now),
inner: inner.clone(), inner: inner.clone(),
}; };
self.content.insert(key.borrow().clone(), entry); self.content.insert(key.borrow().clone(), entry);

View file

@ -1,38 +1,25 @@
use crate::{ use crate::{cache, config::Config, repo::ReadRepoMetadataError, templates};
cache,
config::Config,
repo::{ReadRepoMetadataError, RepoMetadataEntry},
templates,
};
use askama::Template; use askama::Template;
use async_lock::Mutex;
use log::error; use log::error;
use std::{ use std::{
collections::{BTreeMap, BTreeSet, HashMap},
io::{ErrorKind, Read}, io::{ErrorKind, Read},
path::PathBuf, path::PathBuf,
}; };
use trillium::{Conn, Handler}; use trillium::{Conn, Handler};
use trillium_router::{Router, RouterConnExt}; use trillium_router::{Router, RouterConnExt};
pub async fn hello_world(conn: Conn) -> Conn {
conn.ok("hello world!")
}
pub fn make_router(config: &'static Config) -> impl Handler { pub fn make_router(config: &'static Config) -> impl Handler {
let mut hl_registry = giallo::Registry::builtin().unwrap(); let mut hl_registry = giallo::Registry::builtin().unwrap();
hl_registry.link_grammars(); hl_registry.link_grammars();
let hl_registry: &'static _ = Box::leak(Box::new(hl_registry)); let hl_registry: &'static _ = Box::leak(Box::new(hl_registry));
let metadata_cache: &'static _ = Box::leak(Box::new(Mutex::new(cache::Cache::< let metadata_cache: &'static _ = Box::leak(Box::new(cache::Cache::<
String, String,
templates::Directory, templates::Directory,
>::default()))); >::default()));
let file_cache: &'static _ = Box::leak(Box::new(Mutex::new(cache::Cache::< let file_cache: &'static _ =
(String, String), Box::leak(Box::new(cache::Cache::<(String, String), String>::default()));
String,
>::default())));
let client: &'static _ = Box::leak(Box::new(async_lock::Mutex::new( let client: &'static _ = Box::leak(Box::new(async_lock::Mutex::new(
crate::api_client::make_client(), crate::api_client::make_client(),
))); )));
@ -45,10 +32,11 @@ pub fn make_router(config: &'static Config) -> impl Handler {
conn.ok(crate::templates::Home {}.render().unwrap()) conn.ok(crate::templates::Home {}.render().unwrap())
}) })
.post("/fetch", move |mut conn: Conn| async move { .post("/fetch", move |mut conn: Conn| async move {
if let Ok(request_body) = conn.request_body().await.with_max_len(8192).await { if let Ok(request_body) = conn.request_body().with_max_len(8192).read_bytes().await
{
let mut repo_url = None; let mut repo_url = None;
let mut commit_hash = None; let mut commit_hash = None;
for (key, val) in form_urlencoded::parse(request_body.as_bytes()) { for (key, val) in form_urlencoded::parse(&request_body) {
match key.as_ref() { match key.as_ref() {
"repo-url" => { "repo-url" => {
repo_url = Some(val); repo_url = Some(val);
@ -142,11 +130,8 @@ pub fn make_router(config: &'static Config) -> impl Handler {
} }
Some(root) Some(root)
}; };
// TODO replace mutex with better thing (less contention or async mutex) let Some(root) =
let Some(root) = metadata_cache metadata_cache.fetch(repo_hash_str.to_string(), fetch_metadata)
.lock()
.await
.fetch(repo_hash_str.to_string(), fetch_metadata)
else { else {
return conn.with_status(404); return conn.with_status(404);
}; };
@ -178,7 +163,7 @@ pub fn make_router(config: &'static Config) -> impl Handler {
Some(buf) Some(buf)
}; };
let Some(file_content) = file_cache.lock().await.fetch( let Some(file_content) = file_cache.fetch(
(repo_hash_str.to_string(), file_hash.to_string()), (repo_hash_str.to_string(), file_hash.to_string()),
fetch_file, fetch_file,
) else { ) else {

View file

@ -1,7 +1,4 @@
use std::{ use std::{collections::BTreeMap, iter::Peekable};
collections::{BTreeMap, BTreeSet, HashMap, btree_map, btree_set},
iter::Peekable,
};
use log::warn; use log::warn;
use trillium_askama::Template; use trillium_askama::Template;