use super::{FlushMethod, flush_file};
use fusee::local::AlignedBuf;
use fusee_core::vsr::Op;
use std::collections::VecDeque;
use std::fs::File;
use std::io;
use std::os::unix::fs::FileExt;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
pub(super) const IN_FLIGHT: usize = 2;
pub(super) enum WriteBuf {
Plain(Vec<u8>),
Aligned { buf: AlignedBuf, len: usize },
}
impl WriteBuf {
pub fn as_slice(&self) -> &[u8] {
match self {
Self::Plain(v) => v,
Self::Aligned { buf, len } => &buf.as_slice()[..*len],
}
}
}
#[derive(Clone)]
pub(super) struct LogFile {
pub file: Arc<File>,
pub direct: Option<Arc<File>>,
pub method: FlushMethod,
pub refused: Arc<AtomicBool>,
pub dir: PathBuf,
#[cfg(test)]
pub reject_direct: bool,
}
impl LogFile {
pub fn buffered(file: Arc<File>, method: FlushMethod) -> Self {
Self {
file,
direct: None,
method,
refused: Arc::new(AtomicBool::new(false)),
dir: PathBuf::new(),
#[cfg(test)]
reject_direct: false,
}
}
pub fn write(&self, bytes: &[u8], offset: u64) -> io::Result<bool> {
if let Some(d) = self.direct.as_ref()
&& !self.refused.load(Ordering::Acquire)
{
#[cfg(test)]
let r = if self.reject_direct {
Err(io::Error::from_raw_os_error(libc::EINVAL))
} else if self.method == FlushMethod::Dsync {
fusee::local::pwrite_dsync(d, bytes, offset).map(|()| true)
} else {
d.write_all_at(bytes, offset)
.and_then(|()| flush_file(d, self.method))
.map(|()| true)
};
#[cfg(not(test))]
let r = if self.method == FlushMethod::Dsync {
fusee::local::pwrite_dsync(d, bytes, offset).map(|()| true)
} else {
d.write_all_at(bytes, offset)
.and_then(|()| flush_file(d, self.method))
.map(|()| true)
};
match r {
Err(e) if e.raw_os_error() == Some(libc::EINVAL) => {
if !self.refused.swap(true, Ordering::AcqRel) {
fusee::local::note_direct_refused(&self.dir);
}
}
other => return other,
}
}
self.file.write_all_at(bytes, offset)?;
Ok(false)
}
fn write_durable(
&self,
bytes: &[u8],
offset: u64,
delay: std::time::Duration,
) -> io::Result<()> {
let durable = self.write(bytes, offset)?;
if !delay.is_zero() {
std::thread::sleep(delay);
}
if durable {
return Ok(());
}
flush_file(&self.file, self.method)
}
}
struct Job {
seq: u64,
target: LogFile,
offset: u64,
bytes: WriteBuf,
delay: std::time::Duration,
}
#[derive(Default)]
struct Queue {
jobs: VecDeque<Job>,
done: Vec<(u64, io::Result<()>)>,
stop: bool,
}
pub(super) struct LogIo {
queue: Mutex<Queue>,
work: Condvar,
finished: Condvar,
any_done: AtomicBool,
wake_on_done: AtomicBool,
wake: Arc<tokio::sync::Notify>,
}
impl LogIo {
pub fn start(wake: Arc<tokio::sync::Notify>) -> io::Result<Arc<Self>> {
let io = Arc::new(Self {
queue: Mutex::new(Queue::default()),
work: Condvar::new(),
finished: Condvar::new(),
any_done: AtomicBool::new(false),
wake_on_done: AtomicBool::new(true),
wake,
});
for i in 0..IN_FLIGHT {
let io = io.clone();
std::thread::Builder::new()
.name(format!("fusee-replica-log-{i}"))
.spawn(move || io.work())?;
}
Ok(io)
}
fn work(&self) {
let mut q = self.queue.lock().unwrap_or_else(|e| e.into_inner());
loop {
if q.stop {
return;
}
let Some(job) = q.jobs.pop_front() else {
q = self.work.wait(q).unwrap_or_else(|e| e.into_inner());
continue;
};
drop(q);
let result = job
.target
.write_durable(job.bytes.as_slice(), job.offset, job.delay);
q = self.queue.lock().unwrap_or_else(|e| e.into_inner());
q.done.push((job.seq, result));
self.any_done.store(true, Ordering::SeqCst);
self.finished.notify_all();
if job.seq != 0 && self.wake_on_done.load(Ordering::SeqCst) {
self.wake.notify_one();
}
}
}
pub fn has_done(&self) -> bool {
self.any_done.load(Ordering::SeqCst)
}
pub fn stop(&self) {
self.queue.lock().unwrap_or_else(|e| e.into_inner()).stop = true;
self.work.notify_all();
}
}
struct Pending {
seq: u64,
last_op: Op,
padded: bool,
done: bool,
}
pub(super) struct Writes {
io: Arc<LogIo>,
in_flight: VecDeque<Pending>,
waiting: Vec<u8>,
waiting_last: Op,
next_seq: u64,
delay: std::time::Duration,
busy: bool,
}
pub(super) struct Placement {
pub offset: u64,
pub bytes: WriteBuf,
pub padded: bool,
}
impl Writes {
pub fn new(io: Arc<LogIo>, delay: std::time::Duration) -> Self {
Self {
io,
in_flight: VecDeque::new(),
waiting: Vec::new(),
waiting_last: 0,
next_seq: 1,
delay,
busy: false,
}
}
pub fn idle(&self) -> bool {
self.in_flight.is_empty() && self.waiting.is_empty()
}
pub fn has_done(&self) -> bool {
self.io.has_done()
}
pub fn wake_when_done(&self, quorum: bool) {
self.io
.wake_on_done
.store(quorum || !self.waiting.is_empty(), Ordering::SeqCst);
}
pub fn hint(&mut self, file: Arc<File>, bytes: Vec<u8>, method: FlushMethod) {
let mut q = self.io.queue.lock().unwrap_or_else(|e| e.into_inner());
q.jobs.push_back(Job {
seq: 0,
target: LogFile::buffered(file, method),
offset: 0,
bytes: WriteBuf::Plain(bytes),
delay: std::time::Duration::ZERO,
});
drop(q);
self.io.work.notify_one();
}
fn can_start(&self) -> bool {
self.in_flight.len() < IN_FLIGHT && self.in_flight.back().is_none_or(|w| w.padded)
}
pub fn submit(
&mut self,
frames: Vec<u8>,
last_op: Op,
target: &LogFile,
place: &mut dyn FnMut(Vec<u8>, bool) -> io::Result<Placement>,
) -> io::Result<()> {
if self.waiting.is_empty() {
self.waiting = frames;
} else {
self.waiting.extend_from_slice(&frames);
}
self.waiting_last = last_op;
if !self.in_flight.is_empty() {
self.busy = true;
}
self.start(target, place)
}
fn start(
&mut self,
target: &LogFile,
place: &mut dyn FnMut(Vec<u8>, bool) -> io::Result<Placement>,
) -> io::Result<()> {
if self.waiting.is_empty() || !self.can_start() {
return Ok(());
}
let pad = self.busy || !self.in_flight.is_empty();
let p = place(std::mem::take(&mut self.waiting), pad)?;
let seq = self.next_seq;
self.next_seq += 1;
self.in_flight.push_back(Pending {
seq,
last_op: self.waiting_last,
padded: p.padded,
done: false,
});
let mut q = self.io.queue.lock().unwrap_or_else(|e| e.into_inner());
q.jobs.push_back(Job {
seq,
target: target.clone(),
offset: p.offset,
bytes: p.bytes,
delay: self.delay,
});
drop(q);
self.io.work.notify_one();
Ok(())
}
pub fn poll(
&mut self,
target: &LogFile,
place: &mut dyn FnMut(Vec<u8>, bool) -> io::Result<Placement>,
) -> io::Result<Option<Op>> {
if !self.io.has_done() {
return Ok(None);
}
let done = {
let mut q = self.io.queue.lock().unwrap_or_else(|e| e.into_inner());
self.io.any_done.store(false, Ordering::SeqCst);
std::mem::take(&mut q.done)
};
self.complete(done)?;
let durable = self.pop_done();
self.start(target, place)?;
Ok(durable)
}
fn complete(&mut self, done: Vec<(u64, io::Result<()>)>) -> io::Result<()> {
for (seq, result) in done {
result?;
if let Some(p) = self.in_flight.iter_mut().find(|p| p.seq == seq) {
p.done = true;
}
}
Ok(())
}
fn pop_done(&mut self) -> Option<Op> {
let mut durable = None;
while self.in_flight.front().is_some_and(|p| p.done) {
durable = self.in_flight.pop_front().map(|p| p.last_op);
}
if self.idle() {
self.busy = false;
}
durable
}
pub fn drain(
&mut self,
target: &LogFile,
place: &mut dyn FnMut(Vec<u8>, bool) -> io::Result<Placement>,
) -> io::Result<Option<Op>> {
let mut durable = None;
loop {
self.start(target, place)?;
if self.idle() {
break;
}
let done = {
let mut q = self.io.queue.lock().unwrap_or_else(|e| e.into_inner());
while !q.done.iter().any(|(seq, _)| *seq != 0) {
q = self.io.finished.wait(q).unwrap_or_else(|e| e.into_inner());
}
self.io.any_done.store(false, Ordering::SeqCst);
std::mem::take(&mut q.done)
};
self.complete(done)?;
if let Some(op) = self.pop_done() {
durable = Some(op);
}
}
Ok(durable)
}
}
impl Drop for Writes {
fn drop(&mut self) {
self.io.stop();
}
}