use core::pin::Pin;
use futures::{Stream, StreamExt};
use std::fs;
use std::io::Write;
use std::os::fd::AsRawFd;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::watch;
use tokio_stream::wrappers::WatchStream;
use tsoracle_consensus::{ConsensusDriver, ConsensusError, LeaderState};
use tsoracle_core::{Epoch, PHYSICAL_MS_MAX};
use crate::record;
#[derive(Debug, thiserror::Error)]
pub enum FileDriverError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("decode: {0}")]
Decode(#[from] record::RecordError),
#[error("physical_ms {0} exceeds 46-bit maximum")]
PhysicalMsOutOfRange(u64),
}
#[derive(Debug)]
pub struct FileDriver {
dir: PathBuf,
state: Arc<AtomicU64>,
write_lock: tokio::sync::Mutex<()>,
#[allow(dead_code)]
leader_tx: watch::Sender<LeaderState>,
leader_rx: watch::Receiver<LeaderState>,
}
impl FileDriver {
pub fn open_or_init(dir: impl AsRef<Path>) -> Result<Arc<Self>, FileDriverError> {
let dir = dir.as_ref().to_path_buf();
fs::create_dir_all(&dir)?;
let state_path = dir.join("state");
let current = if state_path.exists() {
let bytes = fs::read(&state_path)?;
let high_water = record::decode(&bytes)?;
if high_water > PHYSICAL_MS_MAX {
return Err(FileDriverError::PhysicalMsOutOfRange(high_water));
}
high_water
} else {
0
};
let (tx, rx) = watch::channel(LeaderState::Leader { epoch: Epoch::ZERO });
Ok(Arc::new(FileDriver {
dir,
state: Arc::new(AtomicU64::new(current)),
write_lock: tokio::sync::Mutex::new(()),
leader_tx: tx,
leader_rx: rx,
}))
}
pub fn init_seeded(
dir: impl AsRef<Path>,
seed_physical_ms: u64,
) -> Result<(), FileDriverError> {
if seed_physical_ms > PHYSICAL_MS_MAX {
return Err(FileDriverError::PhysicalMsOutOfRange(seed_physical_ms));
}
let dir = dir.as_ref();
fs::create_dir_all(dir)?;
let state_path = dir.join("state");
if state_path.exists() {
return Err(FileDriverError::Io(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"state file already exists; refusing to overwrite",
)));
}
write_record(dir, seed_physical_ms)?;
Ok(())
}
}
fn write_record(dir: &Path, high_water: u64) -> Result<(), FileDriverError> {
crate::failpoint!(
"file_driver::before_write",
|arg: Option<String>| -> Result<(), FileDriverError> {
let _ = arg; Err(FileDriverError::Io(std::io::Error::other(
"failpoint: file_driver::before_write",
)))
}
);
let tmp = dir.join("state.tmp");
let final_path = dir.join("state");
let bytes = record::encode(high_water);
let mut file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp)?;
file.write_all(&bytes)?;
file.sync_all()?;
drop(file);
crate::failpoint!(
"file_driver::after_tmp_fsync_before_rename",
|arg: Option<String>| -> Result<(), FileDriverError> {
let _ = arg;
Err(FileDriverError::Io(std::io::Error::other(
"failpoint: file_driver::after_tmp_fsync_before_rename",
)))
}
);
fs::rename(&tmp, &final_path)?;
crate::failpoint!("file_driver::after_rename_before_dir_fsync");
let dir_file = fs::File::open(dir)?;
let fd = dir_file.as_raw_fd();
let rc = unsafe { libc::fsync(fd) };
if rc != 0 {
return Err(FileDriverError::Io(std::io::Error::last_os_error()));
}
Ok(())
}
#[async_trait::async_trait]
impl ConsensusDriver for FileDriver {
fn leadership_events(&self) -> Pin<Box<dyn Stream<Item = LeaderState> + Send>> {
Box::pin(WatchStream::new(self.leader_rx.clone()).boxed())
}
async fn load_high_water(&self) -> Result<u64, ConsensusError> {
Ok(self.state.load(Ordering::Acquire))
}
async fn persist_high_water(
&self,
at_least: u64,
_epoch: Epoch,
) -> Result<u64, ConsensusError> {
if at_least > PHYSICAL_MS_MAX {
return Err(ConsensusError::PermanentDriver(Box::new(
FileDriverError::PhysicalMsOutOfRange(at_least),
)));
}
let _guard = self.write_lock.lock().await;
let current = self.state.load(Ordering::Acquire);
if at_least <= current {
return Ok(current);
}
let target = at_least;
let dir = self.dir.clone();
tokio::task::spawn_blocking(move || {
crate::failpoint!("file_driver::write_blocked");
write_record(&dir, target)
})
.await
.map_err(|e| ConsensusError::PermanentDriver(Box::new(std::io::Error::other(e))))?
.map_err(|e| ConsensusError::PermanentDriver(Box::new(e)))?;
self.state.store(target, Ordering::Release);
Ok(target)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn fresh_init_starts_at_zero() {
let dir = tempdir().unwrap();
let driver = FileDriver::open_or_init(dir.path()).unwrap();
assert_eq!(driver.load_high_water().await.unwrap(), 0);
}
#[tokio::test]
async fn persist_then_reload() {
let dir = tempdir().unwrap();
let driver = FileDriver::open_or_init(dir.path()).unwrap();
let actual = driver.persist_high_water(12345, Epoch::ZERO).await.unwrap();
assert_eq!(actual, 12345);
drop(driver);
let reopened = FileDriver::open_or_init(dir.path()).unwrap();
assert_eq!(reopened.load_high_water().await.unwrap(), 12345);
}
#[tokio::test]
async fn persist_is_monotonic() {
let dir = tempdir().unwrap();
let driver = FileDriver::open_or_init(dir.path()).unwrap();
assert_eq!(
driver.persist_high_water(100, Epoch::ZERO).await.unwrap(),
100
);
assert_eq!(
driver.persist_high_water(50, Epoch::ZERO).await.unwrap(),
100
);
assert_eq!(
driver.persist_high_water(200, Epoch::ZERO).await.unwrap(),
200
);
}
#[tokio::test]
async fn init_seeded_rejects_existing_state() {
let dir = tempdir().unwrap();
FileDriver::init_seeded(dir.path(), 1_700_000_000_000).unwrap();
let err = FileDriver::init_seeded(dir.path(), 1_700_000_000_000).unwrap_err();
match err {
FileDriverError::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::AlreadyExists),
_ => panic!("expected AlreadyExists"),
}
}
#[tokio::test]
async fn init_seeded_reloads_as_physical_ms() {
let dir = tempdir().unwrap();
let seed = 1_700_000_000_000u64;
FileDriver::init_seeded(dir.path(), seed).unwrap();
let driver = FileDriver::open_or_init(dir.path()).unwrap();
assert_eq!(driver.load_high_water().await.unwrap(), seed);
assert!(seed < tsoracle_core::PHYSICAL_MS_MAX);
}
#[tokio::test]
async fn init_seeded_rejects_out_of_range_physical_ms() {
let dir = tempdir().unwrap();
let err = FileDriver::init_seeded(dir.path(), PHYSICAL_MS_MAX + 1).unwrap_err();
assert!(matches!(err, FileDriverError::PhysicalMsOutOfRange(_)));
}
#[tokio::test]
async fn persist_rejects_out_of_range_physical_ms() {
let dir = tempdir().unwrap();
let driver = FileDriver::open_or_init(dir.path()).unwrap();
let err = driver
.persist_high_water(PHYSICAL_MS_MAX + 1, Epoch::ZERO)
.await
.unwrap_err();
assert!(matches!(err, ConsensusError::PermanentDriver(_)));
}
#[tokio::test]
async fn open_or_init_rejects_out_of_range_state() {
let dir = tempdir().unwrap();
let state_path = dir.path().join("state");
let bytes = record::encode(PHYSICAL_MS_MAX + 1);
fs::write(&state_path, bytes).unwrap();
let err = FileDriver::open_or_init(dir.path()).unwrap_err();
assert!(
matches!(err, FileDriverError::PhysicalMsOutOfRange(v) if v == PHYSICAL_MS_MAX + 1)
);
}
#[tokio::test]
async fn leadership_events_emits_initial_leader_at_epoch_zero() {
let dir = tempdir().unwrap();
let driver = FileDriver::open_or_init(dir.path()).unwrap();
let mut stream = driver.leadership_events();
let first = tokio::time::timeout(std::time::Duration::from_secs(1), stream.next())
.await
.expect("stream emits initial state within the timeout")
.expect("stream is not closed");
assert_eq!(first, LeaderState::Leader { epoch: Epoch::ZERO });
}
}