use std::fs::File;
use std::io::{BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use mentedb_core::MemoryNode;
use mentedb_core::config::StorageConfig;
use mentedb_core::error::{MenteError, MenteResult};
use mentedb_core::types::MemoryId;
use parking_lot::{Mutex, RwLock};
use tracing::{info, warn};
use crate::cache::{BufferStats, RecordCache};
use crate::legacy::{self, MigrationFault};
use crate::memory_map::{
self, MappedMemory, RecordEntry, RecordTable, Token, entry_stamp, new_token,
};
use crate::record::{
self, BLOCK_SIZE, DATA_FILE, EXTENT_HEADER_SIZE, ExtentHeader, ExtentKind, FreeSpace, PageId,
RecordOp, WAL_FILE,
};
use crate::wal::{Wal, WalEntry, WalEntryType};
struct State {
wal: Wal,
table: RecordTable,
free: FreeSpace,
end: u64,
token: Token,
anchored: bool,
dirty: bool,
}
pub struct StorageEngine {
dir: PathBuf,
file: File,
state: RwLock<State>,
cache: RecordCache,
process_lock: Mutex<Option<File>>,
wal_checkpoint_bytes: u64,
max_record_bytes: u64,
memory_loads: AtomicU64,
disk_reads: AtomicU64,
bytes_read: AtomicU64,
}
fn lock_dir(path: &Path) -> MenteResult<File> {
let lock_file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(path.join("LOCK"))?;
match crate::lock::try_lock_exclusive(&lock_file) {
Ok(true) => Ok(lock_file),
Ok(false) => Err(MenteError::Storage(format!(
"database directory {} is locked by another process",
path.display()
))),
Err(e) => Err(MenteError::Storage(format!(
"failed to lock database directory {}: {e}",
path.display()
))),
}
}
fn create_data_file(dir: &Path) -> MenteResult<()> {
for stale in [WAL_FILE, memory_map::MEMORY_MAP_FILE] {
let p = dir.join(stale);
if p.exists() {
std::fs::remove_file(&p)?;
}
}
let tmp = dir.join(format!("{DATA_FILE}.tmp"));
{
let f = File::create(&tmp)?;
record::write_at(&f, &record::file_header(), 0)?;
f.sync_all()?;
}
std::fs::rename(&tmp, dir.join(DATA_FILE))?;
record::sync_dir(dir)?;
info!(dir = %dir.display(), "created record file");
Ok(())
}
impl StorageEngine {
pub fn open(path: &Path) -> MenteResult<Self> {
Self::open_with_config(path, &StorageConfig::default())
}
pub fn open_with_config(path: &Path, config: &StorageConfig) -> MenteResult<Self> {
Self::open_inner(path, config, None)
}
#[doc(hidden)]
pub fn open_with_migration_fault(
path: &Path,
config: &StorageConfig,
fault: MigrationFault,
) -> MenteResult<Self> {
Self::open_inner(path, config, Some(fault))
}
fn open_inner(
path: &Path,
config: &StorageConfig,
fault: Option<MigrationFault>,
) -> MenteResult<Self> {
std::fs::create_dir_all(path)?;
let lock_file = lock_dir(path)?;
if path.join(DATA_FILE).exists() {
legacy::remove_leftovers(path)?;
} else if legacy::is_legacy(path) {
legacy::migrate(path, fault)?;
} else {
create_data_file(path)?;
}
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path.join(DATA_FILE))?;
let mut header = [0u8; BLOCK_SIZE];
record::read_at(&file, &mut header, 0)?;
record::check_file_header(&header)?;
let wal = Wal::open_file(&path.join(WAL_FILE))?;
let engine = Self {
dir: path.to_path_buf(),
file,
state: RwLock::new(State {
wal,
table: RecordTable::default(),
free: FreeSpace::default(),
end: 1,
token: new_token(),
anchored: false,
dirty: false,
}),
cache: RecordCache::new(config.cache_bytes),
process_lock: Mutex::new(Some(lock_file)),
wal_checkpoint_bytes: config.wal_checkpoint_bytes,
max_record_bytes: config.max_record_bytes,
memory_loads: AtomicU64::new(0),
disk_reads: AtomicU64::new(0),
bytes_read: AtomicU64::new(0),
};
let replayed = engine.recover()?;
let st = engine.state.read();
info!(
?path,
replayed,
records = st.table.records.len(),
bytes = st.end * BLOCK_SIZE as u64,
"storage engine opened"
);
drop(st);
Ok(engine)
}
fn recover(&self) -> MenteResult<usize> {
let mut st = self.state.write();
let entries = st.wal.iterate()?;
let mut ops: Vec<(u64, RecordOp)> = Vec::new();
for e in &entries {
let op = match e.entry_type {
WalEntryType::RecordWrite => RecordOp::decode(true, e.page_id, &e.data)?,
WalEntryType::RecordFree => RecordOp::decode(false, e.page_id, &e.data)?,
WalEntryType::Checkpoint | WalEntryType::Commit => continue,
WalEntryType::PageWrite | WalEntryType::PageFree => {
return Err(MenteError::Storage(
"record WAL holds a page file entry".into(),
));
}
};
op.redo(&self.file)?;
ops.push((e.lsn, op));
}
if !ops.is_empty() {
self.file.sync_data()?;
}
let file_end = self
.file
.metadata()?
.len()
.div_ceil(BLOCK_SIZE as u64)
.max(1);
let (table, token, anchored) = match self.load_snapshot(&entries, file_end) {
Some((table, token)) => (table, token, true),
None => (self.scan_chain(file_end)?, new_token(), false),
};
st.end = file_end.max(table.max_end());
st.free = table.free_space(st.end)?;
st.table = table;
st.token = token;
st.anchored = anchored;
st.dirty = !ops.is_empty();
if !anchored || !ops.is_empty() || st.wal.has_torn_tail() {
self.checkpoint_locked(&mut st, true)?;
}
if !ops.is_empty() {
info!(count = ops.len(), "WAL recovery replayed entries");
}
Ok(ops.len())
}
fn load_snapshot(&self, entries: &[WalEntry], file_end: u64) -> Option<(RecordTable, Token)> {
let snap = match memory_map::read_snapshot(&self.dir) {
Ok(Some(s)) => s,
Ok(None) => return None,
Err(e) => {
warn!(error = %e, "record map snapshot unreadable, open will scan the record file");
return None;
}
};
let Some(at) = entries
.iter()
.rposition(|e| e.entry_type == WalEntryType::Checkpoint && e.data == snap.token)
else {
info!("record map snapshot predates the WAL, open will scan the record file");
return None;
};
let mut table = snap.table;
for e in &entries[at + 1..] {
let write = match e.entry_type {
WalEntryType::RecordWrite => true,
WalEntryType::RecordFree => false,
_ => continue,
};
let op = RecordOp::decode(write, e.page_id, &e.data).ok()?;
table.apply(&op, entry_stamp(&snap.token, e.lsn));
}
if let Err(e) = table.free_space(file_end) {
warn!(error = %e, "record map snapshot does not fit the record file, open will scan");
return None;
}
Some((table, snap.token))
}
fn scan_chain(&self, file_end: u64) -> MenteResult<RecordTable> {
let mut table = RecordTable::default();
let mut reader = BufReader::with_capacity(1 << 20, self.file.try_clone()?);
reader.seek(SeekFrom::Start(BLOCK_SIZE as u64))?;
let mut at = 1u64;
let mut header = [0u8; EXTENT_HEADER_SIZE];
while at < file_end {
reader.read_exact(&mut header)?;
self.disk_reads.fetch_add(1, Ordering::Relaxed);
let h = ExtentHeader::decode(&header)
.ok_or_else(|| MenteError::Storage(format!("record chain broken at block {at}")))?;
let extent = record::Extent {
start: at,
blocks: h.blocks as u64,
};
if h.kind == ExtentKind::Live {
table.records.insert(
h.locator,
RecordEntry {
id: h.id,
stamp: memory_map::random_u64(),
extent,
},
);
table.next_locator = table.next_locator.max(h.locator + 1);
}
at = extent.end();
reader.seek_relative((extent.byte_len() - EXTENT_HEADER_SIZE) as i64)?;
}
self.bytes_read
.fetch_add(file_end * BLOCK_SIZE as u64, Ordering::Relaxed);
table.next_locator = table.next_locator.max(1);
info!(
records = table.records.len(),
"record map rebuilt by scanning the record file"
);
Ok(table)
}
pub fn close(&self) -> MenteResult<()> {
let mut st = self.state.write();
self.file.sync_data()?;
st.wal.sync()?;
drop(st);
if let Some(lock_file) = self.process_lock.lock().take() {
let _ = crate::lock::unlock(&lock_file);
}
info!("storage engine closed");
Ok(())
}
#[doc(hidden)]
pub fn release_process_lock(&self) {
if let Some(lock_file) = self.process_lock.lock().take() {
let _ = crate::lock::unlock(&lock_file);
}
}
pub fn buffer_stats(&self) -> BufferStats {
self.cache.stats()
}
pub fn set_cache_capacity(&self, bytes: u64) {
self.cache.set_capacity(bytes);
}
pub fn cache_capacity(&self) -> u64 {
self.cache.capacity()
}
pub fn page_count(&self) -> u64 {
self.state.read().end
}
pub fn data_bytes(&self) -> u64 {
self.state.read().end * BLOCK_SIZE as u64
}
pub fn free_bytes(&self) -> u64 {
self.state.read().free.free_blocks() * BLOCK_SIZE as u64
}
pub fn memory_loads(&self) -> u64 {
self.memory_loads.load(Ordering::Relaxed)
}
pub fn page_reads(&self) -> u64 {
self.disk_reads.load(Ordering::Relaxed)
}
pub fn bytes_read(&self) -> u64 {
self.bytes_read.load(Ordering::Relaxed)
}
fn encode(&self, node: &MemoryNode) -> MenteResult<Vec<u8>> {
let payload = record::encode_node(node)?;
if payload.len() as u64 > self.max_record_bytes {
return Err(MenteError::CapacityExceeded(format!(
"memory node encoded to {} bytes (max {})",
payload.len(),
self.max_record_bytes
)));
}
Ok(payload)
}
fn maybe_checkpoint(&self, wal_bytes: u64) {
if wal_bytes > self.wal_checkpoint_bytes
&& let Err(e) = self.checkpoint()
{
warn!("auto-checkpoint failed: {e}");
}
}
pub fn store_memory(&self, node: &MemoryNode) -> MenteResult<PageId> {
let ids = self.store_memory_batch(std::slice::from_ref(node))?;
Ok(ids[0])
}
pub fn store_memory_batch(&self, nodes: &[MemoryNode]) -> MenteResult<Vec<PageId>> {
let payloads = nodes
.iter()
.map(|n| self.encode(n))
.collect::<MenteResult<Vec<_>>>()?;
let mut st = self.state.write();
let mut free = st.free.clone();
let mut end = st.end;
let first = st.table.next_locator;
let mut ops = Vec::with_capacity(nodes.len());
for (i, (node, payload)) in nodes.iter().zip(payloads).enumerate() {
let place = free.place(record::blocks_for(payload.len()), end);
end = free.commit(&place, end);
ops.push(RecordOp::Write {
locator: first + i as u64,
id: node.id,
extent: place.extent,
payload,
remainder: place.remainder,
freed: None,
});
}
let mut lsns = Vec::with_capacity(ops.len());
for op in &ops {
lsns.push(
st.wal
.append(WalEntryType::RecordWrite, op.locator(), &op.encode())?,
);
}
st.wal.sync()?;
st.free = free;
st.end = end;
let mut ids = Vec::with_capacity(ops.len());
for ((op, lsn), node) in ops.iter().zip(lsns).zip(nodes) {
op.redo(&self.file)?;
let stamp = entry_stamp(&st.token, lsn);
st.table.apply(op, stamp);
if let RecordOp::Write { payload, .. } = op {
self.cache
.insert(op.locator(), node.clone(), payload.len() as u64);
}
ids.push(PageId(op.locator()));
}
st.dirty = true;
let wal_bytes = st.wal.file_size();
drop(st);
self.maybe_checkpoint(wal_bytes);
Ok(ids)
}
pub fn update_memory(&self, page_id: PageId, node: &MemoryNode) -> MenteResult<()> {
let payload = self.encode(node)?;
let need = record::blocks_for(payload.len());
let len = payload.len() as u64;
let mut st = self.state.write();
let entry =
*st.table.records.get(&page_id.0).ok_or_else(|| {
MenteError::Storage(format!("page {} holds no memory", page_id.0))
})?;
let place = (need > entry.extent.blocks).then(|| st.free.place(need, st.end));
let op = RecordOp::Write {
locator: page_id.0,
id: node.id,
extent: place.map_or(entry.extent, |p| p.extent),
payload,
remainder: place.and_then(|p| p.remainder),
freed: place.map(|_| entry.extent),
};
let lsn = st
.wal
.append(WalEntryType::RecordWrite, page_id.0, &op.encode())?;
st.wal.sync()?;
if let Some(p) = place {
let end = st.end;
st.end = st.free.commit(&p, end);
st.free.insert(entry.extent);
}
op.redo(&self.file)?;
let stamp = entry_stamp(&st.token, lsn);
st.table.apply(&op, stamp);
st.dirty = true;
self.cache.insert(page_id.0, node.clone(), len);
let wal_bytes = st.wal.file_size();
drop(st);
self.maybe_checkpoint(wal_bytes);
Ok(())
}
pub fn delete_memory(&self, page_id: PageId) -> MenteResult<()> {
let mut st = self.state.write();
let entry =
*st.table.records.get(&page_id.0).ok_or_else(|| {
MenteError::Storage(format!("page {} holds no memory", page_id.0))
})?;
let op = RecordOp::Free {
locator: page_id.0,
extent: entry.extent,
};
let _lsn = st
.wal
.append(WalEntryType::RecordFree, page_id.0, &op.encode())?;
st.wal.sync()?;
st.free.insert(entry.extent);
op.redo(&self.file)?;
st.table.apply(&op, 0);
st.dirty = true;
self.cache.invalidate(page_id.0);
let wal_bytes = st.wal.file_size();
drop(st);
self.maybe_checkpoint(wal_bytes);
Ok(())
}
pub fn load_memory(&self, page_id: PageId) -> MenteResult<MemoryNode> {
self.memory_loads.fetch_add(1, Ordering::Relaxed);
if let Some(node) = self.cache.get(page_id.0) {
return Ok(node);
}
let st = self.state.read();
let entry =
st.table.records.get(&page_id.0).ok_or_else(|| {
MenteError::Storage(format!("page {} holds no memory", page_id.0))
})?;
let mut buf = vec![0u8; entry.extent.byte_len()];
record::read_at(&self.file, &mut buf, entry.extent.offset())?;
self.disk_reads.fetch_add(1, Ordering::Relaxed);
self.bytes_read
.fetch_add(buf.len() as u64, Ordering::Relaxed);
let payload = record::live_payload(&buf, page_id.0)?;
let node = record::decode_node(payload)?;
self.cache
.insert(page_id.0, node.clone(), payload.len() as u64);
Ok(node)
}
pub fn checkpoint(&self) -> MenteResult<()> {
let mut st = self.state.write();
self.checkpoint_locked(&mut st, false)
}
fn checkpoint_locked(&self, st: &mut State, force: bool) -> MenteResult<()> {
if !force && !st.dirty && st.anchored {
return Ok(());
}
self.file.sync_data()?;
let token = new_token();
let lsn = st.wal.append(WalEntryType::Checkpoint, 0, &token)?;
st.wal.sync()?;
match memory_map::write_snapshot(&self.dir, &token, &st.table) {
Ok(()) => st.anchored = true,
Err(e) => {
warn!(error = %e, "record map snapshot write failed, next open will scan");
memory_map::remove_snapshot(&self.dir);
st.anchored = false;
}
}
st.token = token;
st.dirty = false;
st.wal.reset_to(lsn, WalEntryType::Checkpoint, 0, &token)?;
info!(lsn, records = st.table.records.len(), "checkpoint complete");
Ok(())
}
pub fn memory_map(&self) -> MenteResult<Vec<MappedMemory>> {
Ok(self.state.read().table.memories())
}
pub fn memory_stamp(&self, page: PageId) -> Option<(MemoryId, u64)> {
self.state
.read()
.table
.records
.get(&page.0)
.map(|e| (e.id, e.stamp))
}
pub fn scan_all_memories(&self) -> Vec<(MemoryId, PageId)> {
let st = self.state.read();
let end = st.end;
let Ok(file) = self.file.try_clone() else {
return Vec::new();
};
let mut reader = BufReader::with_capacity(1 << 20, file);
if reader.seek(SeekFrom::Start(BLOCK_SIZE as u64)).is_err() {
return Vec::new();
}
let mut out = Vec::new();
let mut at = 1u64;
let mut header = [0u8; EXTENT_HEADER_SIZE];
while at < end {
if reader.read_exact(&mut header).is_err() {
break;
}
self.disk_reads.fetch_add(1, Ordering::Relaxed);
let Some(h) = ExtentHeader::decode(&header) else {
break;
};
let mut rest = vec![0u8; h.blocks as usize * BLOCK_SIZE - EXTENT_HEADER_SIZE];
if reader.read_exact(&mut rest).is_err() {
break;
}
if h.kind == ExtentKind::Live {
let mut buf = header.to_vec();
buf.extend_from_slice(&rest);
if let Ok(node) =
record::live_payload(&buf, h.locator).and_then(record::decode_node)
{
out.push((node.id, PageId(h.locator)));
}
}
at += h.blocks as u64;
}
drop(st);
out.sort_by_key(|(_, p)| *p);
out
}
}
#[cfg(test)]
mod tests;