use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, Weak};
use fjall::{Database, Keyspace, PersistMode};
use serde::Deserialize;
use super::{FJALL_MAX_JOURNAL_BYTES, FJALL_WORKER_THREADS, IndexError, open_keyspace};
const MEMORY_CACHE_BYTES: u64 = 8 * 1_024 * 1_024;
const MEMORY_BY_KEY: &str = "memory_by_key";
const MEMORY_ARCHIVE: &str = "memory_archive";
const PROPOSALS: &str = "proposals";
pub(crate) struct MemoryDb {
db: Database,
pub(crate) memory_by_key: Keyspace,
pub(crate) memory_archive: Keyspace,
pub(crate) proposals: Keyspace,
}
impl MemoryDb {
fn open(dir: &Path) -> Result<Self, IndexError> {
std::fs::create_dir_all(dir).map_err(|source| IndexError::Io {
path: dir.to_path_buf(),
source,
})?;
let db = Database::builder(dir)
.cache_size(MEMORY_CACHE_BYTES)
.worker_threads(FJALL_WORKER_THREADS)
.max_journaling_size(FJALL_MAX_JOURNAL_BYTES)
.open()?;
let memory_by_key = open_keyspace(&db, MEMORY_BY_KEY)?;
let memory_archive = open_keyspace(&db, MEMORY_ARCHIVE)?;
let proposals = open_keyspace(&db, PROPOSALS)?;
Ok(Self {
db,
memory_by_key,
memory_archive,
proposals,
})
}
#[cfg(test)]
pub(crate) fn keyspace_names(&self) -> Vec<String> {
self.db.list_keyspace_names().iter().map(ToString::to_string).collect()
}
}
static OPEN_STORES: Mutex<Option<HashMap<PathBuf, Weak<MemoryDb>>>> = Mutex::new(None);
pub(crate) fn open_shared(dir: &Path) -> Result<Arc<MemoryDb>, IndexError> {
std::fs::create_dir_all(dir).map_err(|source| IndexError::Io {
path: dir.to_path_buf(),
source,
})?;
let key = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
let mut guard = OPEN_STORES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let stores = guard.get_or_insert_with(HashMap::new);
stores.retain(|_, weak| weak.strong_count() > 0);
if let Some(open) = stores.get(&key).and_then(Weak::upgrade) {
return Ok(open);
}
let opened = Arc::new(MemoryDb::open(dir)?);
stores.insert(key, Arc::downgrade(&opened));
Ok(opened)
}
const DETACHED_DIR: &str = "memory-detached.fjall";
pub(crate) fn open_detached(view_dir: &Path) -> Result<Arc<MemoryDb>, IndexError> {
let dir = view_dir.join(DETACHED_DIR);
if dir.exists() {
std::fs::remove_dir_all(&dir).map_err(|source| IndexError::Io {
path: dir.clone(),
source,
})?;
}
open_shared(&dir)
}
#[derive(Deserialize, Default)]
struct Stamp {
#[serde(default)]
created_at: i64,
#[serde(default)]
updated_at: i64,
}
fn stamp_of(bytes: &[u8]) -> i64 {
rmp_serde::from_slice::<Stamp>(bytes)
.map(|stamp| stamp.updated_at.max(stamp.created_at))
.unwrap_or(i64::MIN)
}
fn incoming_wins(incoming: &[u8], existing: &[u8]) -> bool {
(stamp_of(incoming), incoming) > (stamp_of(existing), existing)
}
fn merge_keyspace(from: &Keyspace, into: &Keyspace) -> Result<(), IndexError> {
for guard in from.iter() {
let (key, value) = guard.into_inner()?;
let replace = match into.get(&key)? {
Some(existing) => incoming_wins(&value, &existing),
None => true,
};
if replace {
into.insert(key.to_vec(), value.to_vec())?;
}
}
Ok(())
}
pub(crate) fn migrate_legacy(view_db: &Database, memory: &MemoryDb) -> Result<(), IndexError> {
let targets = [
(MEMORY_BY_KEY, &memory.memory_by_key),
(MEMORY_ARCHIVE, &memory.memory_archive),
(PROPOSALS, &memory.proposals),
];
for (name, target) in targets {
if !view_db.keyspace_exists(name) {
continue;
}
let legacy = open_keyspace(view_db, name)?;
merge_keyspace(&legacy, target)?;
memory.db.persist(PersistMode::SyncAll)?;
view_db.delete_keyspace(legacy)?;
}
Ok(())
}
#[cfg(test)]
#[path = "memory_store_tests.rs"]
mod tests;