use alloc::vec::Vec;
use crate::block::{BlockDevice, BlockId, StorageError};
use crate::page::{BufferPool, PAGE_SIZE};
use crate::wal::{RecordKind, Wal};
pub struct StorageEngine<D: BlockDevice> {
pool: BufferPool<D>,
wal: Wal,
}
impl<D: BlockDevice> StorageEngine<D> {
pub fn new(device: D, capacity: usize) -> Self {
Self {
pool: BufferPool::new(device, capacity),
wal: Wal::new(),
}
}
pub const PAGE_SIZE: usize = PAGE_SIZE;
pub fn write_page(&mut self, block_id: BlockId, page: &[u8]) -> Result<(), StorageError> {
if page.len() != PAGE_SIZE {
return Err(StorageError::ShortWrite {
got: page.len(),
expected: PAGE_SIZE,
});
}
self.wal.append(RecordKind::PageWrite, block_id, page);
let frame = self.pool.fetch_mut(block_id)?;
frame.as_bytes_mut().copy_from_slice(page);
self.pool.unpin(block_id);
Ok(())
}
pub fn read_page(&mut self, block_id: BlockId) -> Result<&[u8], StorageError> {
let frame = self.pool.fetch(block_id)?;
Ok(frame.as_bytes())
}
pub fn commit(&mut self) -> Result<(), StorageError> {
self.wal.append(RecordKind::Commit, 0, &[]);
self.pool.flush_all()
}
pub fn wal_bytes(&self) -> &[u8] {
self.wal.as_bytes()
}
pub fn recover(&mut self, log_bytes: &[u8]) -> Result<usize, StorageError> {
let wal = Wal::from_bytes(log_bytes);
let mut applied = 0usize;
let mut pending: Vec<(BlockId, Vec<u8>)> = Vec::new();
wal.replay(|rec| match rec.kind {
RecordKind::PageWrite if rec.payload.len() == PAGE_SIZE => {
pending.push((rec.block_id, rec.payload.clone()));
}
RecordKind::Commit | RecordKind::Checkpoint => {
for (block_id, payload) in pending.drain(..) {
if self
.pool
.device_mut()
.write_block(block_id, &payload)
.is_ok()
{
applied += 1;
}
}
}
_ => {}
});
self.wal = wal;
Ok(applied)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::InMemoryBlockDevice;
fn page_of(byte: u8) -> alloc::vec::Vec<u8> {
alloc::vec![byte; PAGE_SIZE]
}
#[test]
fn write_then_read_round_trips_through_pool() {
let mut e = StorageEngine::new(InMemoryBlockDevice::new(4), 2);
e.write_page(0, &page_of(0xAB)).unwrap();
assert_eq!(e.read_page(0).unwrap()[0], 0xAB);
}
#[test]
fn wal_records_page_before_main_storage() {
let mut e = StorageEngine::new(InMemoryBlockDevice::new(4), 2);
e.write_page(1, &page_of(0x11)).unwrap();
assert!(!e.wal_bytes().is_empty());
e.commit().unwrap();
let log = e.wal_bytes().to_vec();
let wal = Wal::from_bytes(&log);
let mut found = false;
wal.replay(|rec| {
if rec.kind == RecordKind::PageWrite
&& rec.block_id == 1
&& rec.payload.first() == Some(&0x11)
{
found = true;
}
});
assert!(found, "WAL must record the page write before storage");
}
#[test]
fn recover_replays_committed_pages_after_crash() {
let dev = InMemoryBlockDevice::new(4);
let log;
{
let mut e = StorageEngine::new(dev.clone(), 2);
e.write_page(0, &page_of(0x01)).unwrap();
e.write_page(2, &page_of(0x02)).unwrap();
e.commit().unwrap();
log = e.wal_bytes().to_vec();
}
let mut e2 = StorageEngine::new(dev, 2);
let n = e2.recover(&log).unwrap();
assert_eq!(n, 2, "both committed page writes should replay");
assert_eq!(e2.read_page(0).unwrap()[0], 0x01);
assert_eq!(e2.read_page(2).unwrap()[0], 0x02);
}
#[test]
fn recover_ignores_torn_tail() {
let dev = InMemoryBlockDevice::new(4);
let mut e = StorageEngine::new(dev.clone(), 2);
e.write_page(0, &page_of(0x07)).unwrap();
e.commit().unwrap();
let mut log = e.wal_bytes().to_vec();
let len = log.len();
for b in log.iter_mut().skip(len - 2) {
*b ^= 0xFF;
}
let mut e2 = StorageEngine::new(dev, 2);
let n = e2.recover(&log).unwrap();
assert_eq!(
n, 0,
"the only page write's commit marker was torn, so it must not be replayed"
);
assert_eq!(e2.read_page(0).unwrap()[0], 0x00);
}
#[test]
fn recover_drops_page_write_with_no_following_commit() {
let dev = InMemoryBlockDevice::new(4);
let log;
{
let mut e = StorageEngine::new(dev.clone(), 2);
e.write_page(0, &page_of(0x01)).unwrap();
e.commit().unwrap();
e.write_page(1, &page_of(0x02)).unwrap();
log = e.wal_bytes().to_vec();
}
let mut e2 = StorageEngine::new(dev, 2);
let n = e2.recover(&log).unwrap();
assert_eq!(n, 1, "only the committed write (block 0) should replay");
assert_eq!(e2.read_page(0).unwrap()[0], 0x01);
assert_eq!(e2.read_page(1).unwrap()[0], 0x00);
}
}
#[cfg(feature = "std")]
pub struct Database {
engine: StorageEngine<crate::block::FileBlockDevice>,
}
#[cfg(feature = "std")]
impl Database {
pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, StorageError> {
let device = crate::block::FileBlockDevice::open(path)?;
Ok(Self {
engine: StorageEngine::new(device, 64),
})
}
pub fn create<P: AsRef<std::path::Path>>(
path: P,
block_count: u64,
) -> Result<Self, StorageError> {
let device = crate::block::FileBlockDevice::create(path, block_count)?;
Ok(Self {
engine: StorageEngine::new(device, 64),
})
}
pub fn put(&mut self, block_id: BlockId, page: &[u8]) -> Result<(), StorageError> {
self.engine.write_page(block_id, page)?;
self.engine.commit()
}
pub fn get(&mut self, block_id: BlockId) -> Result<&[u8], StorageError> {
self.engine.read_page(block_id)
}
pub fn recover_from(&mut self, log_bytes: &[u8]) -> Result<usize, StorageError> {
self.engine.recover(log_bytes)
}
}
#[cfg(all(test, feature = "std"))]
mod db_tests {
use super::*;
fn temp_db(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"tpt-archon-db-{}-{}-{}.bin",
name,
std::process::id(),
name.len()
));
let _ = std::fs::remove_file(&p);
p
}
fn page_of(byte: u8) -> alloc::vec::Vec<u8> {
alloc::vec![byte; PAGE_SIZE]
}
#[test]
fn create_open_put_and_get() {
let path = temp_db("create");
{
let mut db = Database::create(&path, 4).unwrap();
db.put(1, &page_of(0x55)).unwrap();
}
{
let mut db = Database::open(&path).unwrap();
assert_eq!(db.get(1).unwrap()[0], 0x55);
}
let _ = std::fs::remove_file(&path);
}
}