use std::collections::BTreeMap;
use std::io::Write;
use std::path::Path;
use mentedb_core::error::{MenteError, MenteResult};
use mentedb_core::types::MemoryId;
use crate::record::{Extent, FreeSpace, PageId, RecordOp, le_u32, le_u64};
pub const MEMORY_MAP_FILE: &str = "records_map.bin";
const MAGIC: &[u8; 8] = b"MDBRMAP1";
const FORMAT_VERSION: u32 = 1;
pub(crate) type Token = [u8; 16];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MappedMemory {
pub id: MemoryId,
pub page: PageId,
pub stamp: u64,
}
pub(crate) fn random_u64() -> u64 {
let (a, b) = uuid::Uuid::new_v4().as_u64_pair();
a ^ b.rotate_left(17)
}
pub(crate) fn new_token() -> Token {
*uuid::Uuid::new_v4().as_bytes()
}
pub(crate) fn entry_stamp(token: &Token, lsn: u64) -> u64 {
fn mix(mut z: u64) -> u64 {
z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
let a = le_u64(&token[..8]);
let b = le_u64(&token[8..]);
mix(a ^ mix(b ^ mix(lsn)))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RecordEntry {
pub(crate) id: MemoryId,
pub(crate) stamp: u64,
pub(crate) extent: Extent,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct RecordTable {
pub(crate) records: BTreeMap<u64, RecordEntry>,
pub(crate) next_locator: u64,
}
impl RecordTable {
pub(crate) fn apply(&mut self, op: &RecordOp, stamp: u64) {
match op {
RecordOp::Write {
locator,
id,
extent,
..
} => {
self.records.insert(
*locator,
RecordEntry {
id: *id,
stamp,
extent: *extent,
},
);
}
RecordOp::Free { locator, .. } => {
self.records.remove(locator);
}
}
self.next_locator = self.next_locator.max(op.locator() + 1);
}
pub(crate) fn memories(&self) -> Vec<MappedMemory> {
self.records
.iter()
.map(|(&page, e)| MappedMemory {
id: e.id,
page: PageId(page),
stamp: e.stamp,
})
.collect()
}
pub(crate) fn max_end(&self) -> u64 {
self.records
.values()
.map(|e| e.extent.end())
.max()
.unwrap_or(1)
}
pub(crate) fn free_space(&self, end: u64) -> MenteResult<FreeSpace> {
FreeSpace::from_live(self.records.values().map(|e| e.extent).collect(), end)
}
}
#[derive(Debug, Clone)]
pub(crate) struct Snapshot {
pub(crate) token: Token,
pub(crate) table: RecordTable,
}
fn put_u64(buf: &mut Vec<u8>, v: u64) {
buf.extend_from_slice(&v.to_le_bytes());
}
pub(crate) fn encode(token: &Token, t: &RecordTable) -> Vec<u8> {
let mut buf = Vec::with_capacity(64 + t.records.len() * 56);
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
buf.extend_from_slice(token);
put_u64(&mut buf, t.next_locator);
put_u64(&mut buf, t.records.len() as u64);
for (&locator, e) in &t.records {
put_u64(&mut buf, locator);
buf.extend_from_slice(e.id.0.as_bytes());
put_u64(&mut buf, e.stamp);
put_u64(&mut buf, e.extent.start);
put_u64(&mut buf, e.extent.blocks);
}
let crc = crc32fast::hash(&buf);
buf.extend_from_slice(&crc.to_le_bytes());
buf
}
struct Reader<'a> {
buf: &'a [u8],
pos: usize,
}
impl Reader<'_> {
fn take(&mut self, n: usize) -> MenteResult<&[u8]> {
let end = self
.pos
.checked_add(n)
.filter(|&e| e <= self.buf.len())
.ok_or_else(|| MenteError::Storage("record map snapshot truncated".into()))?;
let s = &self.buf[self.pos..end];
self.pos = end;
Ok(s)
}
fn u64(&mut self) -> MenteResult<u64> {
Ok(le_u64(self.take(8)?))
}
}
pub(crate) fn decode(bytes: &[u8]) -> MenteResult<Snapshot> {
let bad = |what: &str| MenteError::Storage(format!("record map snapshot {what}"));
if bytes.len() < MAGIC.len() + 4 + 16 + 8 + 8 + 4 {
return Err(bad("too short"));
}
let (body, crc_bytes) = bytes.split_at(bytes.len() - 4);
if crc32fast::hash(body) != le_u32(crc_bytes) {
return Err(bad("checksum mismatch"));
}
let mut r = Reader { buf: body, pos: 0 };
if r.take(8)? != MAGIC {
return Err(bad("magic mismatch"));
}
if le_u32(r.take(4)?) != FORMAT_VERSION {
return Err(bad("version mismatch"));
}
let mut token = [0u8; 16];
token.copy_from_slice(r.take(16)?);
let next_locator = r.u64()?;
let n = r.u64()?;
let mut table = RecordTable {
records: BTreeMap::new(),
next_locator,
};
for _ in 0..n {
let locator = r.u64()?;
let id = uuid::Uuid::from_slice(r.take(16)?).map_err(|e| bad(&e.to_string()))?;
let stamp = r.u64()?;
let start = r.u64()?;
let blocks = r.u64()?;
if locator >= next_locator {
return Err(bad("has a locator past its counter"));
}
table.records.insert(
locator,
RecordEntry {
id: MemoryId(id),
stamp,
extent: Extent { start, blocks },
},
);
}
if r.pos != body.len() {
return Err(bad("has trailing bytes"));
}
Ok(Snapshot { token, table })
}
pub(crate) fn read_snapshot(dir: &Path) -> MenteResult<Option<Snapshot>> {
match std::fs::read(dir.join(MEMORY_MAP_FILE)) {
Ok(bytes) => decode(&bytes).map(Some),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
pub(crate) fn write_snapshot(dir: &Path, token: &Token, table: &RecordTable) -> MenteResult<()> {
let path = dir.join(MEMORY_MAP_FILE);
let tmp = dir.join(format!("{MEMORY_MAP_FILE}.tmp"));
{
let mut file = std::fs::File::create(&tmp)?;
file.write_all(&encode(token, table))?;
file.sync_all()?;
}
std::fs::rename(&tmp, &path)?;
Ok(())
}
pub(crate) fn remove_snapshot(dir: &Path) {
let _ = std::fs::remove_file(dir.join(MEMORY_MAP_FILE));
}
#[cfg(test)]
mod tests {
use super::*;
fn snap() -> Snapshot {
let mut table = RecordTable::default();
for (loc, start) in [(1u64, 1u64), (3, 4)] {
table.apply(
&RecordOp::Write {
locator: loc,
id: MemoryId::new(),
extent: Extent { start, blocks: 3 },
payload: vec![],
remainder: None,
freed: None,
},
random_u64(),
);
}
Snapshot {
token: new_token(),
table,
}
}
#[test]
fn snapshot_roundtrips() {
let dir = tempfile::tempdir().unwrap();
let a = snap();
assert_eq!(a.table.next_locator, 4);
write_snapshot(dir.path(), &a.token, &a.table).unwrap();
let b = read_snapshot(dir.path()).unwrap().unwrap();
assert_eq!(a.token, b.token);
assert_eq!(a.table, b.table);
assert!(!dir.path().join(format!("{MEMORY_MAP_FILE}.tmp")).exists());
}
#[test]
fn corrupt_snapshot_is_rejected() {
let dir = tempfile::tempdir().unwrap();
let s = snap();
write_snapshot(dir.path(), &s.token, &s.table).unwrap();
let path = dir.path().join(MEMORY_MAP_FILE);
let mut bytes = std::fs::read(&path).unwrap();
let mid = bytes.len() / 2;
bytes[mid] ^= 0xff;
std::fs::write(&path, &bytes).unwrap();
assert!(read_snapshot(dir.path()).is_err());
std::fs::write(&path, &bytes[..10]).unwrap();
assert!(read_snapshot(dir.path()).is_err());
std::fs::remove_file(&path).unwrap();
assert!(read_snapshot(dir.path()).unwrap().is_none());
}
#[test]
fn free_space_follows_the_table() {
let s = snap();
assert_eq!(s.table.max_end(), 7);
let f = s.table.free_space(10).unwrap();
assert_eq!(f.free_blocks(), 3);
assert!(s.table.free_space(6).is_err(), "extent past the end");
}
}