use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use crate::control::{self, Control, LibraryRef};
use crate::db::{self, Db};
use crate::error::AppError;
use crate::storage::FileScope;
const IDLE_READERS: usize = 4;
pub struct Library {
pub id: LibraryRef,
pub db: Arc<Db>,
pub files: FileScope,
version_high: AtomicI64,
}
pub struct Libraries {
dir: PathBuf,
control: Arc<Control>,
cap: usize,
open: tokio::sync::Mutex<HashMap<LibraryRef, (Arc<Library>, u64)>>,
clock: AtomicU64,
}
pub fn path(dir: &Path, library: LibraryRef) -> PathBuf {
dir.join(format!("{library}.sqlite3"))
}
impl Libraries {
pub fn new(dir: PathBuf, control: Arc<Control>, cap: usize) -> Self {
Libraries {
dir,
control,
cap: cap.max(1),
open: Default::default(),
clock: AtomicU64::new(0),
}
}
#[cfg(test)]
pub fn dir(&self) -> &Path {
&self.dir
}
pub async fn get(&self, id: LibraryRef) -> Result<Arc<Library>, AppError> {
let tick = self.clock.fetch_add(1, Ordering::Relaxed);
let mut open = self.open.lock().await;
if let Some((library, used)) = open.get_mut(&id) {
*used = tick;
return Ok(library.clone());
}
let files = match id {
LibraryRef::User(user_id) => match self.control.user(user_id).await? {
Some(user) if !user.disabled => user.files(),
_ => return Err(AppError::NotFound("no such library".into())),
},
LibraryRef::Group(_) => return Err(AppError::NotFound("no such library".into())),
};
let version_high = self.control.version_high(id).await?;
let file = path(&self.dir, id);
let db = tokio::task::spawn_blocking(move || {
if version_high > 0 && !file.exists() {
return Err(AppError::Internal(format!(
"{} is missing, but the library served version {version_high}: restore \
it from a backup (defter restore)",
file.display()
)));
}
Db::open_with(&file, db::LIBRARY, IDLE_READERS)
})
.await
.map_err(|e| AppError::Internal(format!("blocking task: {e}")))??;
match db.read(|c| crate::store::file_usage(c, "")).await {
Ok((bytes, _)) => self.control.note_usage(id, bytes).await?,
Err(error) => tracing::warn!(%error, library = %id, "counting storage use"),
}
let library = Arc::new(Library {
id,
db,
files,
version_high: AtomicI64::new(version_high),
});
open.insert(id, (library.clone(), tick));
if open.len() > self.cap {
let mut idle: Vec<(LibraryRef, u64)> = open
.iter()
.filter(|(_, (library, _))| Arc::strong_count(library) == 1)
.map(|(id, (_, used))| (*id, *used))
.collect();
idle.sort_by_key(|(_, used)| *used);
let excess = open.len() - self.cap;
for (id, _) in idle.into_iter().take(excess) {
open.remove(&id);
}
}
Ok(library)
}
pub async fn served(&self, library: &Library, version: i64) -> Result<(), AppError> {
if library.version_high.load(Ordering::Relaxed) >= version {
return Ok(());
}
self.control.note_version(library.id, version).await?;
library.version_high.fetch_max(version, Ordering::Relaxed);
Ok(())
}
#[cfg(test)]
pub async fn open_count(&self) -> usize {
self.open.lock().await.len()
}
}
pub fn owner(id: u64, username: &str, legacy_files: bool) -> control::User {
control::User {
id,
orcid: None,
username: username.to_string(),
display_name: None,
profile_hidden: false,
disabled: false,
legacy_files,
quota_bytes: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn setup(cap: usize, users: &[u64]) -> (tempfile::TempDir, Arc<Control>, Libraries) {
let dir = tempfile::tempdir().unwrap();
let control = Arc::new(Control::open(&dir.path().join("control.sqlite3")).unwrap());
for id in users {
let user = owner(*id, &format!("user{id}"), false);
control
.db
.write(move |c| control::insert_user(c, &user))
.await
.unwrap();
}
let libraries = Libraries::new(dir.path().join("libraries"), control.clone(), cap);
(dir, control, libraries)
}
#[tokio::test]
async fn opens_each_library_once() {
let (_dir, _control, libraries) = setup(10, &[1, 2]).await;
let one = libraries.get(LibraryRef::User(1)).await.unwrap();
let again = libraries.get(LibraryRef::User(1)).await.unwrap();
assert!(Arc::ptr_eq(&one.db, &again.db));
assert_eq!(one.files, FileScope::User(1));
let two = libraries.get(LibraryRef::User(2)).await.unwrap();
assert!(!Arc::ptr_eq(&one.db, &two.db));
assert!(path(libraries.dir(), LibraryRef::User(2)).exists());
for unknown in [LibraryRef::User(3), LibraryRef::Group(1)] {
assert!(matches!(
libraries.get(unknown).await,
Err(AppError::NotFound(_))
));
}
}
#[tokio::test]
async fn closes_the_least_recently_used_that_nobody_holds() {
let (_dir, _control, libraries) = setup(2, &[1, 2, 3, 4]).await;
let held = libraries.get(LibraryRef::User(1)).await.unwrap();
drop(libraries.get(LibraryRef::User(2)).await.unwrap());
drop(libraries.get(LibraryRef::User(3)).await.unwrap());
assert_eq!(libraries.open_count().await, 2);
let again = libraries.get(LibraryRef::User(1)).await.unwrap();
assert!(Arc::ptr_eq(&held.db, &again.db));
let three = libraries.get(LibraryRef::User(3)).await.unwrap();
let four = libraries.get(LibraryRef::User(4)).await.unwrap();
assert_eq!(libraries.open_count().await, 3);
drop((three, four, again, held));
}
#[tokio::test]
async fn never_starts_a_library_afresh_that_served_versions() {
let (_dir, control, libraries) = setup(10, &[1]).await;
let library = libraries.get(LibraryRef::User(1)).await.unwrap();
libraries.served(&library, 7).await.unwrap();
libraries.served(&library, 5).await.unwrap();
assert_eq!(control.version_high(LibraryRef::User(1)).await.unwrap(), 7);
drop(library);
let fresh = Libraries::new(libraries.dir().to_path_buf(), control.clone(), 10);
std::fs::remove_file(path(fresh.dir(), LibraryRef::User(1))).unwrap();
let refused = fresh.get(LibraryRef::User(1)).await.err().unwrap();
assert!(
refused.to_string().contains("served version 7"),
"{refused}"
);
}
#[tokio::test]
async fn a_disabled_user_has_no_library() {
let (_dir, control, libraries) = setup(10, &[1]).await;
control
.db
.write(|c| c.execute("UPDATE user SET disabled = 1", []))
.await
.unwrap();
assert!(matches!(
libraries.get(LibraryRef::User(1)).await,
Err(AppError::NotFound(_))
));
}
}