use alloc::sync::Arc;
use core::{cmp::Ordering, hash::Hasher as _};
use aranya_libc::{
self as libc, AsAtRoot, Errno, LOCK_EX, LOCK_NB, O_CLOEXEC, O_CREAT, O_DIRECTORY, O_EXCL,
O_RDONLY, O_RDWR, OwnedDir, OwnedFd, Path, S_IRGRP, S_IRUSR, S_IWGRP, S_IWUSR,
};
use buggy::BugExt as _;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use tracing::{error, warn};
use super::error::Error;
use crate::{
GraphId, StorageError,
linear::{
io::{FactCacheOffset, IoManager, Read, Write},
libc::IdPath,
},
storage::{HeadSet, HeadSetOffset},
};
struct GraphIdIterator {
inner: OwnedDir,
}
impl GraphIdIterator {
fn new(fd: impl AsAtRoot) -> Result<Self, StorageError> {
let fd = libc::dup(fd.as_root())?;
let mut inner = libc::fdopendir(fd)?;
libc::rewinddir(&mut inner);
Ok(Self { inner })
}
}
impl Iterator for GraphIdIterator {
type Item = Result<GraphId, StorageError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let entry = match libc::readdir(&mut self.inner) {
Ok(Some(entry)) => entry,
Ok(None) => return None,
Err(errno) => return Some(Err(errno.into())),
};
let name = entry.name().to_bytes();
if name != b"." && name != b".." {
match GraphId::decode(name) {
Ok(graph_id) => return Some(Ok(graph_id)),
Err(err) => {
warn!(
"Filename {:?} is not a valid GraphId: {}",
entry.name(),
err
);
}
}
}
}
}
}
#[derive(Debug)]
#[clippy::has_significant_drop]
pub struct FileManager {
#[cfg_attr(target_os = "vxworks", allow(dead_code))]
fd: OwnedFd,
#[cfg(target_os = "vxworks")]
dir: aranya_libc::PathBuf,
}
impl FileManager {
pub fn new<P: AsRef<Path>>(dir: P) -> Result<Self, Error> {
let fd = libc::open(dir.as_ref(), O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0)?;
Ok(Self {
fd,
#[cfg(target_os = "vxworks")]
dir: dir.as_ref().to_path_buf(),
})
}
#[cfg(target_os = "vxworks")]
fn root(&self) -> &Path {
&self.dir
}
#[cfg(not(target_os = "vxworks"))]
fn root(&self) -> libc::BorrowedFd<'_> {
libc::AsFd::as_fd(&self.fd)
}
}
impl IoManager for FileManager {
type Writer = Writer;
fn create(&mut self, id: GraphId) -> Result<Self::Writer, StorageError> {
let name = IdPath::new(id);
let fd = libc::openat(
self.root(),
name,
O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP,
)?;
libc::flock(&fd, LOCK_EX | LOCK_NB)?;
Writer::create(fd)
}
fn open(&mut self, id: GraphId) -> Result<Option<Self::Writer>, StorageError> {
let name = IdPath::new(id);
let fd = match libc::openat(self.root(), name, O_RDWR | O_CLOEXEC, 0) {
Ok(fd) => fd,
Err(Errno::ENOENT) => return Ok(None),
Err(e) => return Err(e.into()),
};
libc::flock(&fd, LOCK_EX | LOCK_NB)?;
Ok(Some(Writer::open(fd)?))
}
fn remove(&mut self, id: GraphId) -> Result<(), StorageError> {
let name = IdPath::new(id);
libc::unlinkat(self.root(), name, 0)?;
Ok(())
}
fn list(
&mut self,
) -> Result<impl Iterator<Item = Result<GraphId, StorageError>>, StorageError> {
GraphIdIterator::new(self.root())
}
}
#[derive(Debug)]
pub struct Writer {
file: File,
root: Root,
alloc_end: i64,
next_root: i64,
data_dirty: bool,
}
const PAGE: i64 = 4096;
const ROOT_A: i64 = PAGE;
const ROOT_B: i64 = PAGE * 2;
const FREE_START: i64 = PAGE * 3;
fn other_root(slot: i64) -> i64 {
if slot == ROOT_A { ROOT_B } else { ROOT_A }
}
const PREALLOC_CHUNK: i64 = 4 * 1024 * 1024;
const LEN_PREFIX_LEN: i64 = 4;
impl Writer {
fn create(fd: OwnedFd) -> Result<Self, StorageError> {
let file = File { fd: Arc::new(fd) };
let alloc_end = const { FREE_START + PREALLOC_CHUNK };
file.fallocate(0, alloc_end)?;
Ok(Self {
file,
root: Root::new(),
alloc_end,
next_root: ROOT_A,
data_dirty: false,
})
}
fn open(fd: OwnedFd) -> Result<Self, StorageError> {
let file = File { fd: Arc::new(fd) };
let (root, chosen) = match (
file.load(ROOT_A).and_then(Root::validate),
file.load(ROOT_B).and_then(Root::validate),
) {
(Ok(root_a), Ok(root_b)) => match root_a.generation.cmp(&root_b.generation) {
Ordering::Less => (root_b, ROOT_B),
Ordering::Equal | Ordering::Greater => (root_a, ROOT_A),
},
(Ok(root_a), Err(_)) => (root_a, ROOT_A),
(Err(_), Ok(root_b)) => (root_b, ROOT_B),
(Err(e), Err(_)) => return Err(e),
};
let alloc_end = root.free_offset;
Ok(Self {
file,
root,
alloc_end,
next_root: other_root(chosen),
data_dirty: false,
})
}
fn ensure_capacity(&mut self, end: i64) -> Result<(), StorageError> {
if end <= self.alloc_end {
return Ok(());
}
let mut new_end = self.alloc_end;
while new_end < end {
new_end = new_end
.checked_add(PREALLOC_CHUNK)
.assume("preallocation size fits in `i64`")?;
}
self.file.fallocate(0, new_end)?;
self.alloc_end = new_end;
Ok(())
}
fn append_at<F, T>(&mut self, builder: F) -> Result<(T, u64), StorageError>
where
F: FnOnce(u64) -> T,
T: Serialize,
{
let offset = self.root.free_offset;
let off: u64 = offset
.try_into()
.assume("`free_offset` can be converted to `u64`")?;
let item = builder(off);
let bytes = postcard::to_allocvec(&item).map_err(|err| {
error!(?err, "append");
StorageError::IoError
})?;
let len = i64::try_from(bytes.len()).assume("serialized len fits in `i64`")?;
let end = offset
.checked_add(LEN_PREFIX_LEN)
.and_then(|o| o.checked_add(len))
.assume("append stays within `i64`")?;
self.ensure_capacity(end)?;
let new_offset = self.file.dump_bytes(offset, &bytes)?;
self.root.free_offset = new_offset;
self.data_dirty = true;
Ok((item, off))
}
fn fetch_owned<T: DeserializeOwned>(&self, offset: u64) -> Result<T, StorageError> {
let off = i64::try_from(offset).assume("`offset` can be converted to `i64`")?;
self.file.load(off)
}
fn write_root(&mut self) -> Result<(), StorageError> {
self.root.generation = self
.root
.generation
.checked_add(1)
.assume("generation will not overflow u64")?;
self.root.checksum = self.root.calc_checksum();
let slot = self.next_root;
self.file.dump(slot, &self.root)?;
self.file.sync()?;
self.next_root = other_root(slot);
Ok(())
}
}
impl Write for Writer {
type ReadOnly = Reader;
fn readonly(&self) -> Self::ReadOnly {
Reader {
file: self.file.clone(),
}
}
fn heads(&self) -> Result<HeadSet, StorageError> {
let offset = self.root.heads.ok_or(StorageError::NotInitialized)?;
self.fetch_owned(offset)
}
fn heads_offset(&self) -> Result<HeadSetOffset, StorageError> {
let offset = self.root.heads.ok_or(StorageError::NotInitialized)?;
Ok(HeadSetOffset::new(offset))
}
fn fact_cache(&self) -> Result<FactCacheOffset, StorageError> {
let offset = self.root.fact_cache.ok_or(StorageError::NotInitialized)?;
Ok(FactCacheOffset::new(offset))
}
fn append<F, T>(&mut self, builder: F) -> Result<T, StorageError>
where
F: FnOnce(u64) -> T,
T: Serialize,
{
let (item, _) = self.append_at(builder)?;
Ok(item)
}
fn commit(&mut self, heads: &HeadSet, fact_cache: FactCacheOffset) -> Result<(), StorageError> {
let (_, heads_offset) = self.append_at(|_| heads.clone())?;
self.root.heads = Some(heads_offset);
self.root.fact_cache = Some(fact_cache.get());
if self.data_dirty {
self.file.sync()?;
self.data_dirty = false;
}
self.write_root()?;
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
struct Root {
generation: u64,
heads: Option<u64>,
fact_cache: Option<u64>,
free_offset: i64,
checksum: u64,
}
impl Root {
fn new() -> Self {
Self {
generation: 0,
heads: None,
fact_cache: None,
free_offset: FREE_START,
checksum: 0,
}
}
fn calc_checksum(&self) -> u64 {
let mut hasher = aranya_crypto::dangerous::siphasher::sip::SipHasher::new();
hasher.write_u64(self.generation);
for offset in [self.heads, self.fact_cache] {
match offset {
Some(offset) => {
hasher.write_u8(1);
hasher.write_u64(offset);
}
None => hasher.write_u8(0),
}
}
hasher.write_i64(self.free_offset);
hasher.finish()
}
fn validate(self) -> Result<Self, StorageError> {
if self.checksum != self.calc_checksum() {
tracing::warn!("invalid checksum");
return Err(StorageError::IoError);
}
Ok(self)
}
}
#[derive(Clone, Debug)]
pub struct Reader {
file: File,
}
impl Read for Reader {
fn fetch<T>(&self, offset: u64) -> Result<T, StorageError>
where
T: DeserializeOwned,
{
let off = i64::try_from(offset).assume("`offset` can be converted to `i64`")?;
self.file.load(off)
}
}
#[derive(Clone, Debug)]
struct File {
fd: Arc<OwnedFd>,
}
impl File {
fn fallocate(&self, offset: i64, len: i64) -> Result<(), StorageError> {
libc::fallocate(&self.fd, 0, offset, len)?;
libc::fsync(&self.fd)?;
Ok(())
}
fn read_exact(&self, mut offset: i64, mut buf: &mut [u8]) -> Result<(), StorageError> {
while !buf.is_empty() {
match libc::pread(&self.fd, buf, offset) {
Ok(0) => break,
Ok(n) => {
buf = buf.get_mut(n..).assume("`n` should be in bounds")?;
offset = offset
.checked_add(i64::try_from(n).assume("read within bounds")?)
.assume("read within bounds")?;
}
Err(Errno::EINTR) => {}
Err(e) => return Err(e.into()),
}
}
if !buf.is_empty() {
error!(remaining = buf.len(), "could not fill buffer");
return Err(StorageError::IoError);
}
Ok(())
}
fn write_all(&self, mut offset: i64, mut buf: &[u8]) -> Result<(), StorageError> {
while !buf.is_empty() {
match libc::pwrite(&self.fd, buf, offset) {
Ok(0) => {
error!(remaining = buf.len(), "could not write whole buffer");
return Err(StorageError::IoError);
}
Ok(n) => {
buf = buf.get(n..).assume("`n` is in bounds")?;
offset = offset
.checked_add(i64::try_from(n).assume("write within bounds")?)
.assume("write within bounds")?;
}
Err(Errno::EINTR) => {}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
fn sync(&self) -> Result<(), StorageError> {
libc::fdatasync(&self.fd)?;
Ok(())
}
fn dump<T: Serialize>(&self, offset: i64, value: &T) -> Result<i64, StorageError> {
let bytes = postcard::to_allocvec(value).map_err(|err| {
error!(?err, "dump");
StorageError::IoError
})?;
self.dump_bytes(offset, &bytes)
}
fn dump_bytes(&self, offset: i64, bytes: &[u8]) -> Result<i64, StorageError> {
let len: u32 = bytes
.len()
.try_into()
.assume("serialized objects should fit in u32")?;
self.write_all(offset, &len.to_be_bytes())?;
let offset2 = offset
.checked_add(LEN_PREFIX_LEN)
.assume("offset not near u64::MAX")?;
self.write_all(offset2, bytes)?;
let off = offset2
.checked_add(len.into())
.assume("offset valid after write")?;
Ok(off)
}
fn load<T: DeserializeOwned>(&self, offset: i64) -> Result<T, StorageError> {
let mut bytes = [0u8; 4];
self.read_exact(offset, &mut bytes)?;
let len = u32::from_be_bytes(bytes);
let mut bytes = alloc::vec![0u8; len as usize];
self.read_exact(
offset
.checked_add(LEN_PREFIX_LEN)
.assume("offset not near u64::MAX")?,
&mut bytes,
)?;
postcard::from_bytes(&bytes).map_err(|err| {
error!(?err, "load");
StorageError::IoError
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
CmdId, MaxCut, SegmentIndex,
storage::{HeadSet, LocatedAddress},
};
fn located(id: u8, seg: u64, max_cut: u64) -> LocatedAddress {
let mut bytes = [0u8; 32];
bytes[0] = id;
LocatedAddress {
id: CmdId::from_bytes(bytes),
segment: SegmentIndex::new(seg),
max_cut: MaxCut::new(max_cut),
}
}
fn heads(id: u8) -> HeadSet {
HeadSet::single(located(id, id.into(), id.into()))
}
fn graph_id() -> GraphId {
"test".parse().unwrap()
}
fn manager() -> (tempfile::TempDir, FileManager) {
let dir = tempfile::tempdir().unwrap();
let manager = FileManager::new(dir.path()).unwrap();
(dir, manager)
}
#[test]
fn test_reopen_discards_uncommitted_appends() {
let (_dir, mut manager) = manager();
let id = graph_id();
let mut writer = manager.create(id).unwrap();
writer.append(|_| 1u64).unwrap();
writer.commit(&heads(1), FactCacheOffset::new(1)).unwrap();
let committed_offset = writer.root.free_offset;
writer.append(|_| 2u64).unwrap();
writer.append(|_| 3u64).unwrap();
assert_ne!(writer.root.free_offset, committed_offset);
drop(writer);
let writer = manager.open(id).unwrap().unwrap();
assert_eq!(writer.heads().unwrap(), heads(1));
assert_eq!(writer.root.free_offset, committed_offset);
}
#[test]
fn test_reopen_survives_corrupt_root() {
let (_dir, mut manager) = manager();
let id = graph_id();
let mut writer = manager.create(id).unwrap();
writer.append(|_| 1u64).unwrap();
writer.commit(&heads(1), FactCacheOffset::new(1)).unwrap(); writer.append(|_| 2u64).unwrap();
writer.commit(&heads(2), FactCacheOffset::new(2)).unwrap();
writer.file.write_all(ROOT_B, &[0xFF; 64]).unwrap();
drop(writer);
let mut writer = manager.open(id).unwrap().unwrap();
assert_eq!(writer.heads().unwrap(), heads(1));
assert_eq!(writer.next_root, ROOT_B);
writer.commit(&heads(3), FactCacheOffset::new(3)).unwrap();
drop(writer);
let writer = manager.open(id).unwrap().unwrap();
assert_eq!(writer.heads().unwrap(), heads(3));
}
#[test]
fn test_root_slots_ping_pong_across_reopen() {
let (_dir, mut manager) = manager();
let id = graph_id();
let mut writer = manager.create(id).unwrap();
writer.commit(&heads(1), FactCacheOffset::new(1)).unwrap(); writer.commit(&heads(2), FactCacheOffset::new(2)).unwrap(); drop(writer);
let mut writer = manager.open(id).unwrap().unwrap();
assert_eq!(writer.heads().unwrap(), heads(2));
assert_eq!(writer.root.generation, 2);
assert_eq!(writer.next_root, ROOT_A);
writer.commit(&heads(3), FactCacheOffset::new(3)).unwrap();
drop(writer);
let writer = manager.open(id).unwrap().unwrap();
assert_eq!(writer.heads().unwrap(), heads(3));
assert_eq!(writer.root.generation, 3);
assert_eq!(writer.next_root, ROOT_B);
}
#[test]
fn head_set_and_fact_cache_round_trip() {
let tempdir = tempfile::tempdir().unwrap();
let mut manager = FileManager::new(tempdir.path()).unwrap();
let graph_id = GraphId::transmute(CmdId::from_bytes([7u8; 32]));
let mut heads = HeadSet::single(located(1, 1, 3));
heads.push(located(2, 2, 5));
assert_eq!(heads.len(), 2);
{
let mut writer = manager.create(graph_id).unwrap();
assert!(matches!(writer.heads(), Err(StorageError::NotInitialized)));
assert!(matches!(
writer.fact_cache(),
Err(StorageError::NotInitialized)
));
writer.commit(&heads, FactCacheOffset::new(1234)).unwrap();
assert_eq!(writer.heads().unwrap(), heads);
assert_eq!(writer.fact_cache().unwrap(), FactCacheOffset::new(1234));
}
let writer = manager.open(graph_id).unwrap().unwrap();
assert_eq!(writer.heads().unwrap(), heads);
assert_eq!(writer.fact_cache().unwrap(), FactCacheOffset::new(1234));
}
}