use crate::db::Hash;
use crate::errors::{error, io_error, yaml_error};
use colored::Colorize;
use futures::future;
use log::trace;
use rogue_logging::Error;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs::File;
use std::io::{BufReader, BufWriter, Write};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::thread;
use std::time::{Duration, Instant};
use tokio::fs::{read_dir, remove_file, OpenOptions};
use tokio::task;
use tokio::time::sleep;
const CHUNK_FILE_EXTENSION: &str = "yml";
const LOCK_ACQUIRE_SLEEP_MILLIS: u64 = 50;
const LOCK_ACQUIRE_TIMEOUT: u64 = 2;
const LOCK_FILE_EXTENSION: &str = "lock";
pub struct Table<const K: usize, const C: usize, T> {
pub(crate) directory: PathBuf,
pub phantom: PhantomData<T>,
}
impl<const K: usize, const C: usize, T> Table<K, C, T> {
pub fn new(directory: PathBuf) -> Self {
Self {
directory,
phantom: PhantomData,
}
}
fn get_chunk_path(&self, hash: Hash<C>) -> PathBuf {
self.directory
.join(format!("{hash}.{CHUNK_FILE_EXTENSION}"))
}
}
impl<const K: usize, const C: usize, T> Default for Table<K, C, T> {
fn default() -> Self {
Self::new(PathBuf::new())
}
}
impl<const K: usize, const C: usize, T> Table<K, C, T>
where
T: Clone + for<'de> Deserialize<'de>,
{
pub fn get(&self, hash: Hash<K>) -> Result<Option<T>, Error> {
let chunk_path = self.get_chunk_path(get_chunk_hash(hash));
if chunk_path.exists() {
let chunk = read_chunk::<K, C, T>(&chunk_path)?;
Ok(chunk.get(&hash).cloned())
} else {
Ok(None)
}
}
pub async fn get_all(&self) -> Result<BTreeMap<Hash<K>, T>, Error> {
let mut items = BTreeMap::new();
let mut dir = read_dir(&self.directory)
.await
.map_err(|e| io_error(e, "read directory"))?;
while let Some(entry) = dir
.next_entry()
.await
.map_err(|e| io_error(e, "read entry"))?
{
let path = entry.path();
let extension = path
.extension()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if !path.is_file() || extension != CHUNK_FILE_EXTENSION {
trace!("Skipping non-chunk file: {}", path.display());
continue;
}
let chunk = read_chunk::<K, C, T>(&path)?;
items.extend(chunk);
}
Ok(items)
}
}
#[allow(dead_code)]
impl<const K: usize, const C: usize, T> Table<K, C, T>
where
T: Clone + Send + Serialize + for<'de> Deserialize<'de> + 'static,
{
pub async fn set(&self, hash: Hash<K>, item: T) -> Result<(), Error> {
let chunk_path = self.get_chunk_path(get_chunk_hash(hash));
let lock = acquire_lock(&chunk_path).await?;
let mut chunk = if chunk_path.exists() {
read_chunk::<K, C, T>(&chunk_path)?
} else {
BTreeMap::new()
};
chunk.insert(hash, item.clone());
write_chunk::<K, C, T>(chunk_path, chunk)?;
release_lock(lock).await?;
Ok(())
}
pub async fn set_many(
&self,
items: BTreeMap<Hash<K>, T>,
replace: bool,
) -> Result<usize, Error> {
let chunks = group_by_chunk(items);
let futures = chunks.into_iter().map(|(chunk_hash, new_chunk)| {
let chunk_path = self.get_chunk_path(chunk_hash);
task::spawn(
async move { update_chunk::<K, C, T>(chunk_path, new_chunk, replace).await },
)
});
let mut added = 0;
for result in future::join_all(futures).await {
added += result.unwrap_or_else(|e| {
Err(Error {
action: "update table".to_owned(),
message: e.to_string(),
domain: Some("tokio".to_owned()),
..Error::default()
})
})?;
}
Ok(added)
}
}
fn get_chunk_hash<const K: usize, const C: usize>(hash: Hash<K>) -> Hash<C> {
hash.truncate::<C>().expect("should be able to truncate")
}
fn group_by_chunk<const K: usize, const C: usize, T>(
items: BTreeMap<Hash<K>, T>,
) -> BTreeMap<Hash<C>, BTreeMap<Hash<K>, T>> {
let mut chunks: BTreeMap<Hash<C>, BTreeMap<Hash<K>, T>> = BTreeMap::new();
for (hash, item) in items {
let chunk_hash = get_chunk_hash(hash);
chunks.entry(chunk_hash).or_insert_with(|| BTreeMap::new());
chunks
.get_mut(&chunk_hash)
.expect("should be created in not exist")
.insert(hash, item);
}
chunks
}
fn read_chunk<const K: usize, const C: usize, T>(
path: &PathBuf,
) -> Result<BTreeMap<Hash<K>, T>, Error>
where
T: for<'de> Deserialize<'de>,
{
if !path.exists() || !path.is_file() {
return Err(error(
"read chunk",
format!("Chunk file does not exist: {}", path.display()),
));
}
trace!("{} chunk file: {}", "Reading".bold(), path.display());
let file = File::open(path).map_err(|e| io_error(e, "open chunk file"))?;
let reader = BufReader::new(file);
serde_yaml::from_reader(reader).map_err(|e| yaml_error(e, "deserialize chunk"))
}
fn write_chunk<const K: usize, const C: usize, T>(
path: PathBuf,
chunk: BTreeMap<Hash<K>, T>,
) -> Result<(), Error>
where
T: Serialize,
{
trace!("{} chunk file: {}", "Writing".bold(), path.display());
let file = File::create(path).map_err(|e| io_error(e, "create chunk file"))?;
let mut writer = BufWriter::new(file);
serde_yaml::to_writer(&mut writer, &chunk).map_err(|e| yaml_error(e, "write chunk"))?;
writer
.flush()
.map_err(|e| error("flush chunk", e.to_string()))?;
Ok(())
}
async fn update_chunk<const K: usize, const C: usize, T>(
chunk_path: PathBuf,
new_chunk: BTreeMap<Hash<K>, T>,
replace: bool,
) -> Result<usize, Error>
where
T: for<'de> Deserialize<'de> + Serialize,
{
let mut added = 0;
let lock = acquire_lock(&chunk_path).await?;
let mut chunk = if chunk_path.exists() {
read_chunk::<K, C, T>(&chunk_path)?
} else {
BTreeMap::new()
};
for (hash, item) in new_chunk {
if replace || !chunk.contains_key(&hash) {
chunk.insert(hash, item);
added += 1;
}
}
write_chunk::<K, C, T>(chunk_path, chunk)?;
release_lock(lock).await?;
Ok(added)
}
async fn acquire_lock(path: &Path) -> Result<PathBuf, Error> {
let start = Instant::now();
let timeout = Duration::from_secs(LOCK_ACQUIRE_TIMEOUT);
let mut lock: PathBuf = path.to_path_buf();
lock.set_extension(LOCK_FILE_EXTENSION);
loop {
if OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock)
.await
.is_ok()
{
return Ok(lock);
}
if start.elapsed() > timeout {
return Err(error(
"acquire lock",
format!("Exceeded timeout for acquiring lock: {}", lock.display()),
));
}
sleep(Duration::from_millis(LOCK_ACQUIRE_SLEEP_MILLIS)).await;
}
}
async fn release_lock(path: PathBuf) -> Result<(), Error> {
remove_file(path)
.await
.map_err(|e| io_error(e, "release lock"))
}