use crate::env::{Env, Notify};
use crate::local::{FlushMethod, LocalEnv, LocalOptions, flush_file, sync_dir, unix_ms};
use fusee_core::record::{Lsn, RecordBuf, RunId};
use fusee_core::segment::Batch;
use fusee_core::shard::BlobRef;
use fusee_core::store::Checkpoint;
use fusee_core::wal::{
BATCH_HEADER, BLOB_ALIGN, BLOCK, End, Large, MAX_BATCH, OffloadRef, Progress, SECTION_HEADER,
SectionRef, WAL_HEADER_BYTES, WalHeader, WalScanner, WalWriter, batch_body_len, batch_span,
find_large, for_each_offload, section_records_resolved,
};
use std::collections::BTreeMap;
use std::fs::{self, File, OpenOptions};
use std::io;
use std::os::unix::fs::FileExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex, Weak};
use std::time::Instant;
pub trait LogDevice {
fn segments(&self) -> io::Result<Vec<u64>>;
fn len(&self, segment: u64) -> io::Result<u64>;
fn read(&self, segment: u64, offset: u64, len: usize) -> io::Result<Vec<u8>>;
fn create(&self, segment: u64, len: u64) -> io::Result<()>;
fn write(&self, segment: u64, offset: u64, bytes: &[u8]) -> io::Result<()>;
fn sync(&self, segment: u64) -> io::Result<()>;
fn rename(&self, from: u64, to: u64) -> io::Result<()>;
fn remove(&self, segment: u64) -> io::Result<()>;
fn prepare(&self, segment: u64, len: u64) -> io::Result<()> {
self.create(segment, len)
}
fn is_ready(&self, _segment: u64) -> bool {
true
}
}
pub struct FileDevice {
dir: PathBuf,
ext: &'static str,
opts: LocalOptions,
open: Mutex<BTreeMap<u64, Arc<File>>>,
direct: Option<Direct>,
refused: std::sync::atomic::AtomicBool,
#[cfg(test)]
reject_direct: std::sync::atomic::AtomicBool,
preparing: Mutex<BTreeMap<u64, Arc<Preparation>>>,
preparers: Mutex<Vec<std::thread::JoinHandle<()>>>,
}
impl FileDevice {
pub fn open(dir: &Path, opts: LocalOptions) -> io::Result<Self> {
fs::create_dir_all(dir)?;
let direct = Direct::probe(dir, &opts);
Ok(Self {
dir: dir.to_owned(),
ext: "wal",
opts,
open: Mutex::new(BTreeMap::new()),
direct,
refused: std::sync::atomic::AtomicBool::new(false),
#[cfg(test)]
reject_direct: std::sync::atomic::AtomicBool::new(false),
preparing: Mutex::new(BTreeMap::new()),
preparers: Mutex::new(Vec::new()),
})
}
pub fn blobs(dir: &Path, opts: LocalOptions) -> Self {
Self {
dir: dir.to_owned(),
ext: "blob",
opts,
open: Mutex::new(BTreeMap::new()),
direct: None,
refused: std::sync::atomic::AtomicBool::new(false),
#[cfg(test)]
reject_direct: std::sync::atomic::AtomicBool::new(false),
preparing: Mutex::new(BTreeMap::new()),
preparers: Mutex::new(Vec::new()),
}
}
pub fn direct_io(&self) -> bool {
self.direct.is_some() && !self.refused.load(Ordering::Relaxed)
}
fn direct(&self) -> Option<&Direct> {
self.direct
.as_ref()
.filter(|_| !self.refused.load(Ordering::Relaxed))
}
#[cfg(all(target_os = "linux", feature = "io-uring"))]
fn direct_buffer(
&self,
segment: u64,
offset: u64,
bytes: &[u8],
) -> io::Result<Option<(Arc<File>, AlignedBuf, DirectTail)>> {
let Some(d) = self.direct() else {
return Ok(None);
};
self.ready(segment)?;
let path = self.path(segment);
let (f, buf, _, tail) = d.buffer(&path, segment, offset, bytes, || self.file(segment))?;
Ok(Some((f, buf, tail)))
}
#[cfg(all(target_os = "linux", feature = "io-uring"))]
fn direct_wrote(&self, tail: DirectTail) {
if let Some(d) = self.direct.as_ref() {
d.wrote(tail);
}
}
fn ready(&self, segment: u64) -> io::Result<()> {
let job = self
.preparing
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&segment)
.cloned();
let Some(p) = job else {
return Ok(());
};
let mut r = p.result.lock().unwrap_or_else(|e| e.into_inner());
while r.is_none() {
r = p.done.wait(r).unwrap_or_else(|e| e.into_inner());
}
let result = r.clone().unwrap_or(Ok(()));
drop(r);
let mut map = self.preparing.lock().unwrap_or_else(|e| e.into_inner());
if map.get(&segment).is_some_and(|q| Arc::ptr_eq(q, &p)) {
map.remove(&segment);
}
result.map_err(|(kind, message)| io::Error::new(kind, message))
}
fn fill(dir: &Path, path: &Path, len: u64, opts: &LocalOptions) -> io::Result<()> {
static ONE_AT_A_TIME: Mutex<()> = Mutex::new(());
let _turn = ONE_AT_A_TIME.lock().unwrap_or_else(|e| e.into_inner());
let mut o = OpenOptions::new();
o.read(true).write(true).create(true).truncate(true);
#[cfg(target_os = "linux")]
let direct = {
use std::os::unix::fs::OpenOptionsExt;
let mut d = o.clone();
d.custom_flags(libc::O_DIRECT);
d.open(path).ok()
};
#[cfg(not(target_os = "linux"))]
let direct: Option<File> = None;
let mut direct_fill = direct.is_some();
let mut file = match direct {
Some(f) => f,
None => o.open(path)?,
};
if matches!(
opts.preallocate,
crate::local::Preallocate::Zero | crate::local::Preallocate::ShortFirst
) {
const CHUNK: usize = 256 << 10;
let zeros = aligned_zeros(CHUNK);
let started = Instant::now();
let rate = opts.prepare_bytes_per_sec.max(1) as f64;
let mut off = 0u64;
while off < len {
let n = (len - off).min(CHUNK as u64) as usize;
match file.write_all_at(&zeros[..n], off) {
Ok(()) => {}
Err(e) if direct_fill && e.raw_os_error() == Some(libc::EINVAL) => {
direct_fill = false;
file = OpenOptions::new().read(true).write(true).open(path)?;
continue;
}
Err(e) => return Err(e),
}
off += n as u64;
let due = std::time::Duration::from_secs_f64(off as f64 / rate);
if let Some(ahead) = due.checked_sub(started.elapsed()) {
std::thread::sleep(ahead);
}
}
} else {
file.set_len(len)?;
}
flush_file(&file, opts.flush)?;
sync_dir(dir, opts.flush)
}
fn path(&self, segment: u64) -> PathBuf {
self.dir.join(format!("{segment:020}.{}", self.ext))
}
fn file(&self, segment: u64) -> io::Result<Arc<File>> {
let mut open = self.open.lock().unwrap_or_else(|e| e.into_inner());
if let Some(f) = open.get(&segment) {
return Ok(f.clone());
}
let f = Arc::new(
OpenOptions::new()
.read(true)
.write(true)
.open(self.path(segment))?,
);
open.insert(segment, f.clone());
while open.len() > 4 {
if let Some((&first, _)) = open.iter().next() {
open.remove(&first);
}
}
Ok(f)
}
fn forget(&self, segment: u64) {
self.open
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&segment);
if let Some(d) = &self.direct {
d.forget(segment);
}
}
}
impl LogDevice for FileDevice {
fn is_ready(&self, segment: u64) -> bool {
let job = self
.preparing
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&segment)
.cloned();
job.is_none_or(|p| p.result.lock().unwrap_or_else(|e| e.into_inner()).is_some())
}
fn segments(&self) -> io::Result<Vec<u64>> {
let entries = match fs::read_dir(&self.dir) {
Ok(d) => d,
Err(e) if e.kind() == io::ErrorKind::NotFound && self.ext != "wal" => {
return Ok(Vec::new());
}
Err(e) => return Err(e),
};
let mut v: Vec<u64> = entries
.filter_map(|e| e.ok())
.filter_map(|e| {
e.file_name()
.to_str()?
.strip_suffix(self.ext)?
.strip_suffix('.')?
.parse()
.ok()
})
.collect();
v.sort_unstable();
Ok(v)
}
fn len(&self, segment: u64) -> io::Result<u64> {
self.ready(segment)?;
Ok(fs::metadata(self.path(segment))?.len())
}
fn read(&self, segment: u64, offset: u64, len: usize) -> io::Result<Vec<u8>> {
self.ready(segment)?;
let f = self.file(segment)?;
let size = f.metadata()?.len();
let end = offset.saturating_add(len as u64).min(size);
let mut buf = vec![0; end.saturating_sub(offset) as usize];
f.read_exact_at(&mut buf, offset.min(end))?;
Ok(buf)
}
fn create(&self, segment: u64, len: u64) -> io::Result<()> {
self.ready(segment)?;
if !self.dir.exists() {
fs::create_dir_all(&self.dir)?;
if let Some(parent) = self.dir.parent() {
sync_dir(parent, self.opts.flush)?;
}
}
let path = self.path(segment);
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)?;
match self.opts.preallocate {
crate::local::Preallocate::Zero | crate::local::Preallocate::ShortFirst => {
let zeros = vec![0u8; 1 << 20];
let mut off = 0;
while off < len {
let n = (len - off).min(zeros.len() as u64) as usize;
file.write_all_at(&zeros[..n], off)?;
off += n as u64;
}
}
_ => file.set_len(len)?,
}
flush_file(&file, self.opts.flush)?;
sync_dir(&self.dir, self.opts.flush)?;
self.forget(segment);
Ok(())
}
fn write(&self, segment: u64, offset: u64, bytes: &[u8]) -> io::Result<()> {
self.ready(segment)?;
if let Some(d) = self.direct() {
let path = self.path(segment);
#[cfg(test)]
let rejected = self.reject_direct.load(Ordering::Relaxed);
#[cfg(not(test))]
let rejected = false;
let written = if rejected {
Err(io::Error::from_raw_os_error(libc::EINVAL))
} else {
d.write(
&path,
segment,
offset,
bytes,
self.opts.flush == FlushMethod::Dsync,
|| self.file(segment),
)
};
match written {
Err(e) if e.raw_os_error() == Some(libc::EINVAL) => {
if !self.refused.swap(true, Ordering::Relaxed) {
eprintln!(
"fusee: an O_DIRECT write to {} (offset {offset}, {} bytes) failed: {e}; \
the log continues with buffered writes",
path.display(),
bytes.len()
);
}
}
other => return other,
}
}
let f = self.file(segment)?;
if self.opts.flush == FlushMethod::Dsync {
return crate::local::pwrite_dsync(&f, bytes, offset);
}
f.write_all_at(bytes, offset)
}
fn sync(&self, segment: u64) -> io::Result<()> {
if !self.opts.emulated_flush.is_zero() {
std::thread::sleep(self.opts.emulated_flush);
}
if self.opts.flush == FlushMethod::Dsync {
return Ok(());
}
if let Some(d) = self.direct() {
return flush_file(&*d.file(&self.path(segment), segment)?, self.opts.flush);
}
flush_file(&*self.file(segment)?, self.opts.flush)
}
fn rename(&self, from: u64, to: u64) -> io::Result<()> {
self.ready(from)?;
self.ready(to)?;
self.forget(from);
self.forget(to);
fs::rename(self.path(from), self.path(to))?;
sync_dir(&self.dir, self.opts.flush)
}
fn remove(&self, segment: u64) -> io::Result<()> {
self.ready(segment)?;
self.forget(segment);
fs::remove_file(self.path(segment))?;
sync_dir(&self.dir, self.opts.flush)
}
fn prepare(&self, segment: u64, len: u64) -> io::Result<()> {
self.ready(segment)?;
let (dir, path, opts) = (self.dir.clone(), self.path(segment), self.opts.clone());
let p = Arc::new(Preparation::default());
self.preparing
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(segment, p.clone());
let job = std::thread::Builder::new()
.name("fusee-segment".into())
.spawn(move || {
let r = Self::fill(&dir, &path, len, &opts);
*p.result.lock().unwrap_or_else(|e| e.into_inner()) =
Some(r.map_err(|e| (e.kind(), e.to_string())));
p.done.notify_all();
});
let job = match job {
Ok(j) => j,
Err(e) => {
self.preparing
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&segment);
return Err(e);
}
};
let mut threads = self.preparers.lock().unwrap_or_else(|e| e.into_inner());
threads.retain(|t| !t.is_finished());
threads.push(job);
Ok(())
}
}
impl Drop for FileDevice {
fn drop(&mut self) {
let jobs = std::mem::take(&mut *self.preparers.lock().unwrap_or_else(|e| e.into_inner()));
for j in jobs {
let _ = j.join();
}
}
}
#[derive(Default)]
struct Preparation {
result: Mutex<Option<Result<(), (io::ErrorKind, String)>>>,
done: Condvar,
}
const DIRECT_ALIGN: usize = 4096;
#[cfg_attr(not(all(target_os = "linux", feature = "io-uring")), allow(dead_code))]
type DirectTail = Option<(u64, u64, Vec<u8>)>;
struct Direct {
files: Mutex<BTreeMap<u64, Arc<File>>>,
tail: Mutex<Option<(u64, u64, Vec<u8>)>>,
}
impl Direct {
fn probe(dir: &Path, opts: &LocalOptions) -> Option<Self> {
if !opts.direct_io || !cfg!(target_os = "linux") {
return None;
}
let probe = dir.join(".direct-probe");
let ok = Self::open_direct(&probe).is_ok();
let _ = fs::remove_file(&probe);
if !ok {
eprintln!(
"fusee: {} does not support O_DIRECT; the log uses buffered writes",
dir.display()
);
return None;
}
Some(Self {
files: Mutex::new(BTreeMap::new()),
tail: Mutex::new(None),
})
}
#[cfg(target_os = "linux")]
fn open_direct(path: &Path) -> io::Result<File> {
use std::os::unix::fs::OpenOptionsExt;
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.custom_flags(libc::O_DIRECT)
.open(path)
}
#[cfg(not(target_os = "linux"))]
fn open_direct(_path: &Path) -> io::Result<File> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"O_DIRECT needs Linux",
))
}
fn file(&self, path: &Path, segment: u64) -> io::Result<Arc<File>> {
let mut files = self.files.lock().unwrap_or_else(|e| e.into_inner());
if let Some(f) = files.get(&segment) {
return Ok(f.clone());
}
let f = Arc::new(Self::open_direct(path)?);
files.insert(segment, f.clone());
while files.len() > 4 {
if let Some((&first, _)) = files.iter().next() {
files.remove(&first);
}
}
Ok(f)
}
fn forget(&self, segment: u64) {
self.files
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&segment);
let mut tail = self.tail.lock().unwrap_or_else(|e| e.into_inner());
if tail.as_ref().is_some_and(|t| t.0 == segment) {
*tail = None;
}
}
fn write(
&self,
path: &Path,
segment: u64,
offset: u64,
bytes: &[u8],
dsync: bool,
buffered: impl FnOnce() -> io::Result<Arc<File>>,
) -> io::Result<()> {
let (f, buf, block, tail) = self.buffer(path, segment, offset, bytes, buffered)?;
if dsync {
crate::local::pwrite_dsync(&f, buf.as_ref(), block)?;
} else {
f.write_all_at(buf.as_ref(), block)?;
}
self.wrote(tail);
Ok(())
}
fn wrote(&self, tail: Option<(u64, u64, Vec<u8>)>) {
*self.tail.lock().unwrap_or_else(|e| e.into_inner()) = tail;
}
#[allow(clippy::type_complexity)]
fn buffer(
&self,
path: &Path,
segment: u64,
offset: u64,
bytes: &[u8],
buffered: impl FnOnce() -> io::Result<Arc<File>>,
) -> io::Result<(Arc<File>, AlignedBuf, u64, Option<(u64, u64, Vec<u8>)>)> {
let align = DIRECT_ALIGN as u64;
let block = offset / align * align;
let prefix: Vec<u8> = if block == offset {
Vec::new()
} else {
let cached = self.tail.lock().unwrap_or_else(|e| e.into_inner()).clone();
match cached {
Some((s, b, data))
if s == segment && b == block && data.len() as u64 == offset - block =>
{
data
}
_ => {
let mut data = vec![0; (offset - block) as usize];
buffered()?.read_exact_at(&mut data, block)?;
data
}
}
};
let total = prefix.len() + bytes.len();
let padded = total.div_ceil(DIRECT_ALIGN) * DIRECT_ALIGN;
let mut buf = AlignedBuf::zeroed(padded);
buf.as_mut()[..prefix.len()].copy_from_slice(&prefix);
buf.as_mut()[prefix.len()..total].copy_from_slice(bytes);
let f = self.file(path, segment)?;
let end = offset + bytes.len() as u64;
let end_block = end / align * align;
let tail = (end != end_block).then(|| {
(
segment,
end_block,
buf.as_ref()[(end_block - block) as usize..total].to_vec(),
)
});
Ok((f, buf, block, tail))
}
}
struct AlignedBuf {
ptr: std::ptr::NonNull<u8>,
len: usize,
}
impl AlignedBuf {
fn zeroed(len: usize) -> Self {
let layout = std::alloc::Layout::from_size_align(len.max(DIRECT_ALIGN), DIRECT_ALIGN)
.expect("aligned layout");
let raw = unsafe { std::alloc::alloc_zeroed(layout) };
let ptr =
std::ptr::NonNull::new(raw).unwrap_or_else(|| std::alloc::handle_alloc_error(layout));
Self { ptr, len }
}
fn as_mut(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
}
fn as_ref(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
}
}
impl Drop for AlignedBuf {
fn drop(&mut self) {
let layout = std::alloc::Layout::from_size_align(self.len.max(DIRECT_ALIGN), DIRECT_ALIGN)
.expect("aligned layout");
unsafe { std::alloc::dealloc(self.ptr.as_ptr(), layout) }
}
}
unsafe impl Send for AlignedBuf {}
fn aligned_zeros(len: usize) -> &'static [u8] {
#[repr(C, align(4096))]
struct Block([u8; 4096]);
static ZEROS: std::sync::OnceLock<&'static [u8]> = std::sync::OnceLock::new();
let z = ZEROS.get_or_init(|| {
let blocks: Vec<Block> = (0..(256 << 10) / 4096).map(|_| Block([0; 4096])).collect();
let b: &'static mut [Block] = Box::leak(blocks.into_boxed_slice());
unsafe { std::slice::from_raw_parts(b.as_ptr().cast::<u8>(), b.len() * 4096) }
});
&z[..len.min(z.len())]
}
#[derive(Clone, Copy, Debug)]
pub struct WalOptions {
pub group: u32,
pub segment_bytes: u64,
pub first_segment_bytes: u64,
pub spares: usize,
pub in_flight: usize,
}
impl Default for WalOptions {
fn default() -> Self {
Self {
group: 0,
segment_bytes: fusee_core::segment::DEFAULT_SEGMENT_BYTES,
first_segment_bytes: 1 << 20,
spares: 2,
in_flight: log_tuning().in_flight,
}
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct LogTuning {
pub in_flight: usize,
pub pad_always: bool,
pub second_now: bool,
}
pub(crate) fn log_tuning() -> LogTuning {
static T: std::sync::OnceLock<LogTuning> = std::sync::OnceLock::new();
*T.get_or_init(|| LogTuning {
in_flight: std::env::var("FUSEE_LOG_IN_FLIGHT")
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.map_or(2, |n| n.clamp(1, 8)),
pad_always: std::env::var("FUSEE_LOG_PAD").is_ok_and(|v| v == "always"),
second_now: std::env::var("FUSEE_LOG_SECOND").is_ok_and(|v| v == "now"),
})
}
pub struct Blobs<D> {
pub dev: D,
pub threshold: u64,
}
pub struct BlobJob {
pub id: u64,
pub file: u64,
pub offset: u64,
pub create: bool,
data: Arc<Vec<u8>>,
range: std::ops::Range<usize>,
}
impl BlobJob {
pub fn bytes(&self) -> &[u8] {
&self.data[self.range.clone()]
}
pub fn perform<D: LogDevice>(&self, dev: &D) -> io::Result<()> {
if self.create {
dev.create(self.file, 0)?;
}
dev.write(self.file, self.offset, self.bytes())
}
}
impl std::fmt::Debug for BlobJob {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BlobJob")
.field("id", &self.id)
.field("file", &self.file)
.field("offset", &self.offset)
.field("len", &self.range.len())
.finish()
}
}
struct Held {
batch: Batch,
needs: u64,
files: Vec<u64>,
}
#[derive(Clone, Copy, Debug, Default)]
struct BlobFile {
last_ref: u64,
pending: u64,
}
struct BlobArea<D> {
dev: D,
threshold: u64,
file_bytes: u64,
active: Option<(u64, u64)>,
next_file: u64,
files: BTreeMap<u64, BlobFile>,
jobs: std::collections::VecDeque<BlobJob>,
next_id: u64,
written: u64,
held: Vec<std::collections::VecDeque<Held>>,
records: u64,
bytes: u64,
}
impl<D: LogDevice> BlobArea<D> {
fn alloc(&mut self, len: u64) -> (u64, u64, bool) {
let span = len.div_ceil(BLOB_ALIGN) * BLOB_ALIGN;
if let Some((file, at)) = self.active
&& at + span <= self.file_bytes
{
self.active = Some((file, at + span));
return (file, at, false);
}
let file = self.next_file;
self.next_file += 1;
self.active = Some((file, span));
self.files.insert(file, BlobFile::default());
(file, 0, true)
}
fn retire(&mut self, first: u64) -> io::Result<()> {
let dead: Vec<u64> = self
.files
.iter()
.filter(|(_, f)| f.pending == 0 && f.last_ref < first)
.map(|(n, _)| *n)
.collect();
for n in dead {
self.files.remove(&n);
if self.active.is_some_and(|a| a.0 == n) {
self.active = None;
}
match self.dev.remove(n) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct BlobStats {
pub files: usize,
pub records: u64,
pub bytes: u64,
pub held: usize,
pub jobs: usize,
}
#[derive(Default)]
struct Recovered {
sections: Vec<(Lsn, u32, Vec<u8>)>,
last: Lsn,
}
#[derive(Debug)]
pub struct InFlight {
segment: u64,
seq: u64,
shards: Vec<(u32, Lsn)>,
io: Option<(Option<Vec<u8>>, u64, Vec<u8>)>,
}
impl InFlight {
pub fn segment(&self) -> u64 {
self.segment
}
pub fn seq(&self) -> u64 {
self.seq
}
}
#[derive(Debug)]
struct Pending {
seq: u64,
padded: bool,
done: Option<Vec<(u32, Lsn)>>,
}
#[derive(Clone, Copy, Debug)]
struct LiveSegment {
number: u64,
base_seq: u64,
len: u64,
}
pub struct Wal<D: LogDevice> {
dev: D,
opts: WalOptions,
writer: WalWriter,
acked: u64,
staged: Vec<Option<Batch>>,
durable: Vec<Lsn>,
live: Vec<LiveSegment>,
spares: Vec<u64>,
spares_fit: Option<u64>,
in_flight: std::collections::VecDeque<Pending>,
prepared: Option<(u64, u64)>,
next_number: u64,
retain_lsn: Vec<Option<Lsn>>,
holding: Vec<std::collections::VecDeque<(u64, Lsn)>>,
backup_needs: Option<u64>,
recovered: Vec<Recovered>,
rng: u64,
pub(crate) bytes_written: u64,
pub(crate) writes: u64,
pub(crate) rolls: u64,
rolled_at: (u64, u64, u64),
blobs: Option<BlobArea<D>>,
staged_refs: Vec<Vec<u64>>,
batch_limit: usize,
seal_from: usize,
}
fn io_other(e: impl std::fmt::Display) -> io::Error {
io::Error::other(e.to_string())
}
fn split_section(b: &mut Batch, room: usize) -> io::Result<Option<(Batch, usize)>> {
let total = b.section.len();
let mut r = fusee_core::wire::Reader::new(&b.section);
let (mut end, mut records, mut refs) = (0usize, 0u32, 0usize);
while !r.is_empty() {
let body = r
.blob()
.map_err(|e| io_other(format!("staged section: {e}")))?;
let at = total - r.remaining();
if at > room {
break;
}
end = at;
records += 1;
refs += usize::from(fusee_core::wal::is_offloaded(body));
}
if records == 0 {
return Ok(None);
}
if records > b.records {
return Err(io_other("staged section holds more records than it says"));
}
let head = Batch {
chunks: Vec::new(),
first_lsn: b.first_lsn,
last_lsn: b.first_lsn + u64::from(records) - 1,
records,
bytes: end as u64,
section: b.section[..end].to_vec(),
};
b.section.drain(..end);
b.first_lsn += u64::from(records);
b.records -= records;
b.bytes = b.bytes.saturating_sub(end as u64);
Ok(Some((head, refs)))
}
impl<D: LogDevice> Wal<D> {
pub fn open(dev: D, shards: u32, opts: WalOptions, life: u64, now_ms: u64) -> io::Result<Self> {
Self::open_inner(dev, None, shards, opts, life, now_ms)
}
pub fn open_with_blobs(
dev: D,
blobs: Blobs<D>,
shards: u32,
opts: WalOptions,
life: u64,
now_ms: u64,
) -> io::Result<Self> {
Self::open_inner(dev, Some(blobs), shards, opts, life, now_ms)
}
fn open_inner(
dev: D,
blobs: Option<Blobs<D>>,
shards: u32,
opts: WalOptions,
life: u64,
now_ms: u64,
) -> io::Result<Self> {
let mut rng = life | 1;
let blob_files = match &blobs {
Some(b) => b.dev.segments()?,
None => Vec::new(),
};
let scan_refs = !blob_files.is_empty();
let mut blob_refs: BTreeMap<u64, u64> = BTreeMap::new();
let mut recovered: Vec<Recovered> = (0..shards).map(|_| Recovered::default()).collect();
let mut holding: Vec<std::collections::VecDeque<(u64, Lsn)>> =
(0..shards).map(|_| Default::default()).collect();
let mut live: Vec<LiveSegment> = Vec::new();
let mut spares = Vec::new();
let mut chain: Option<(u64, u64)> = None; let mut resume: Option<(u64, u64, u64)> = None; let mut stale: Vec<u64> = Vec::new();
let numbers = dev.segments()?;
let next_number = numbers.last().map_or(1, |n| n + 1);
for number in numbers {
let len = dev.len(number)?;
let head = dev.read(number, 0, WAL_HEADER_BYTES as usize)?;
let header = match WalHeader::decode(&head) {
Ok(Some(h)) if h.segment == number => h,
Err(e @ fusee_core::wal::WalError::UnsupportedFormat(_)) => {
return Err(io_other(e));
}
Ok(None) => {
spares.push(number);
continue;
}
_ => {
stale.push(number);
continue;
}
};
if let Some((seq, checksum)) = chain
&& (header.base_seq != seq + 1 || header.base_prev != checksum)
{
stale.push(number);
continue;
}
let mut first_read = 256 << 10;
let mut bytes = dev.read(number, 0, first_read)?;
let Some(mut scanner) =
WalScanner::start(&bytes, bytes.len() as u64 >= len).map_err(io_other)?
else {
spares.push(number);
continue;
};
let mut visit_err = None;
let scanned = loop {
let complete = bytes.len() as u64 >= len;
let progress = scanner.advance(&bytes, complete, len as usize, &mut |b| {
for s in b.sections() {
let Some(r) = recovered.get_mut(s.shard as usize) else {
visit_err.get_or_insert_with(|| {
format!(
"log batch {} names shard {} of {shards}",
b.header.seq, s.shard
)
});
continue;
};
if r.last != 0 && s.first_lsn != r.last + 1 {
visit_err.get_or_insert_with(|| {
format!(
"shard {} records {}..{} are missing from the log",
s.shard,
r.last + 1,
s.first_lsn.saturating_sub(1)
)
});
}
r.last = s.first_lsn + u64::from(s.count) - 1;
r.sections.push((s.first_lsn, s.count, s.records.to_vec()));
holding[s.shard as usize].push_back((b.header.seq, r.last));
if scan_refs
&& let Err(e) = for_each_offload(s.records, |_, r| {
blob_refs.insert(r.file, b.header.seq);
})
{
visit_err.get_or_insert_with(|| {
format!("log batch {}, shard {}: {e}", b.header.seq, s.shard)
});
}
}
});
match progress {
Progress::Done(s) => break s,
Progress::NeedMore => {
first_read = (first_read * 2).min(16 << 20);
let more = dev.read(number, bytes.len() as u64, first_read)?;
if more.is_empty() {
return Err(io_other(format!(
"reading log segment {number} made no progress"
)));
}
bytes.extend_from_slice(&more);
}
}
};
if let Some(e) = visit_err {
return Err(io_other(e));
}
if let End::Damaged { at, later_seq } = scanned.tail {
return Err(io_other(format!(
"log segment {number} is damaged at offset {at} (batch {}), but batch \
{later_seq} after it says it had been acknowledged: acknowledged records \
were lost. Restore from a replica or backup.",
scanned.last_seq + 1
)));
}
live.push(LiveSegment {
number,
base_seq: header.base_seq,
len,
});
chain = Some((scanned.last_seq, scanned.last_checksum));
resume = Some((number, header.salt, scanned.end as u64));
}
let end = chain.map_or(0, |c| c.0);
for &number in &stale {
let len = dev.len(number)? as usize;
let bytes = dev.read(number, 0, len)?;
if let Ok(Some(mut scanner)) = WalScanner::start(&bytes, true) {
let mut proof = None;
let _ = scanner.advance(&bytes, true, len, &mut |b| {
if b.header.acked > end {
proof.get_or_insert(b.header.seq);
}
});
if let Some(seq) = proof {
return Err(io_other(format!(
"log segment {number} holds batch {seq}, written after batch {} had been \
acknowledged, but the log ends at batch {end}: acknowledged records were \
lost. Restore from a replica or backup.",
end + 1
)));
}
}
dev.write(number, 0, &vec![0; WAL_HEADER_BYTES as usize])?;
dev.sync(number)?;
spares.push(number);
}
for s in &live {
dev.sync(s.number)?;
}
let mut next_number = next_number;
let writer = match (resume, chain) {
(Some((segment, salt, end)), Some((seq, checksum))) => {
let len = live.last().map_or(0, |s| s.len);
WalWriter::resume(opts.group, life, segment, salt, len, end, seq + 1, checksum)
}
_ => {
let number = spares.pop().unwrap_or_else(|| {
next_number += 1;
next_number - 1
});
let len = opts.first_segment_bytes.min(opts.segment_bytes);
dev.create(number, len)?;
rng = xorshift(rng);
let header = WalHeader {
group: opts.group,
segment: number,
salt: rng,
base_seq: 1,
base_prev: 0,
created_ms: now_ms,
};
dev.write(number, 0, &header.encode())?;
dev.sync(number)?;
live.push(LiveSegment {
number,
base_seq: 1,
len,
});
WalWriter::resume(
opts.group,
life,
number,
header.salt,
len,
WAL_HEADER_BYTES,
1,
0,
)
}
};
let acked = writer.next_seq() - 1;
let durable = recovered.iter().map(|r| r.last).collect();
let blobs = match blobs {
None => None,
Some(Blobs {
dev: bdev,
threshold,
}) => {
let mut files = BTreeMap::new();
for &n in &blob_files {
match blob_refs.get(&n) {
Some(&last_ref) => {
files.insert(
n,
BlobFile {
last_ref,
pending: 0,
},
);
}
None => bdev.remove(n)?,
}
}
Some(BlobArea {
dev: bdev,
threshold,
file_bytes: opts.segment_bytes.max(1 << 20),
active: None,
next_file: blob_files.last().map_or(1, |n| n + 1),
files,
jobs: std::collections::VecDeque::new(),
next_id: 1,
written: 0,
held: (0..shards).map(|_| Default::default()).collect(),
records: 0,
bytes: 0,
})
}
};
Ok(Self {
dev,
opts,
writer,
acked,
staged: (0..shards).map(|_| None).collect(),
durable,
live,
spares,
spares_fit: None,
prepared: None,
in_flight: std::collections::VecDeque::new(),
next_number,
retain_lsn: vec![None; shards as usize],
holding,
backup_needs: None,
recovered,
rng,
bytes_written: 0,
writes: 0,
rolls: 0,
rolled_at: (now_ms, 0, now_ms),
blobs,
staged_refs: (0..shards).map(|_| Vec::new()).collect(),
batch_limit: MAX_BATCH,
seal_from: 0,
})
}
#[cfg(test)]
fn limit_batches(&mut self, bytes: usize) {
self.batch_limit = bytes.min(MAX_BATCH);
}
pub fn take_recovered(
&mut self,
shard: u32,
after: Lsn,
) -> io::Result<(Vec<(Lsn, RecordBuf)>, Lsn)> {
let r = self
.recovered
.get_mut(shard as usize)
.ok_or_else(|| io_other(format!("no shard {shard}")))?;
let mut out = Vec::new();
let mut expected = after + 1;
let blobs = self.blobs.as_ref().map(|b| &b.dev);
for (first, count, bytes) in std::mem::take(&mut r.sections) {
let last = first + u64::from(count) - 1;
if last <= after {
continue;
}
if first > expected {
return Err(io_other(format!(
"shard {shard} records {expected}..{} are missing from the log",
first - 1
)));
}
let s = SectionRef {
shard,
first_lsn: first,
count,
records: &bytes,
};
out.extend(resolve_records(&s, after, blobs)?);
expected = last + 1;
}
let last = r.last.max(after);
self.durable[shard as usize] = self.durable[shard as usize].max(last);
Ok((out, last))
}
pub fn take_recovered_sections(&mut self, shard: u32) -> Vec<RawSection> {
self.recovered
.get_mut(shard as usize)
.map(|r| std::mem::take(&mut r.sections))
.unwrap_or_default()
}
pub fn stage(&mut self, shard: u32, batch: Batch) -> io::Result<()> {
let large = match &self.blobs {
Some(b) if b.threshold > 0 && batch.bytes > b.threshold => {
find_large(&batch.section, b.threshold as usize).map_err(io_other)?
}
_ => Vec::new(),
};
self.stage_found(shard, batch, large)
}
pub fn stage_found(
&mut self,
shard: u32,
mut batch: Batch,
large: Vec<Large>,
) -> io::Result<()> {
let s = shard as usize;
if s >= self.staged.len() {
return Err(io_other(format!("no shard {shard}")));
}
let Some(area) = self
.blobs
.as_mut()
.filter(|a| !a.held[s].is_empty() || !large.is_empty())
else {
return self.stage_plain(shard, batch, Vec::new());
};
if large.is_empty() {
area.held[s].push_back(Held {
batch,
needs: 0,
files: Vec::new(),
});
return Ok(());
}
let data = Arc::new(std::mem::take(&mut batch.section));
let first = &large[0];
let prefix = (first.index > 0).then(|| Batch {
chunks: Vec::new(),
first_lsn: batch.first_lsn,
last_lsn: batch.first_lsn + u64::from(first.index) - 1,
records: first.index,
bytes: first.at as u64,
section: data[..first.at].to_vec(),
});
let mut rest = Vec::with_capacity(
data.len() - first.at - large.iter().map(|l| l.body.len()).sum::<usize>()
+ 32 * large.len(),
);
let mut pos = first.at;
let mut files = Vec::with_capacity(large.len());
let mut needs = 0;
for l in &large {
rest.extend_from_slice(&data[pos..l.at]);
let len = l.body.len() as u64;
let (file, offset, create) = area.alloc(len);
let id = area.next_id;
area.next_id += 1;
area.jobs.push_back(BlobJob {
id,
file,
offset,
create,
data: data.clone(),
range: l.body.clone(),
});
OffloadRef {
kind: data[l.body.start],
file,
offset,
len,
crc: l.crc,
}
.encode(&mut rest);
if let Some(f) = area.files.get_mut(&file) {
f.pending += 1;
}
files.push(file);
area.records += 1;
area.bytes += len;
needs = id;
pos = l.body.end;
}
rest.extend_from_slice(&data[pos..]);
let held = Held {
batch: Batch {
chunks: Vec::new(),
first_lsn: batch.first_lsn + u64::from(first.index),
last_lsn: batch.last_lsn,
records: batch.records - first.index,
bytes: rest.len() as u64,
section: rest,
},
needs,
files,
};
let waiting = !area.held[s].is_empty();
match prefix {
Some(p) if waiting => area.held[s].push_back(Held {
batch: p,
needs: 0,
files: Vec::new(),
}),
Some(p) => {
area.held[s].push_back(held);
return self.stage_plain(shard, p, Vec::new());
}
None => {}
}
area.held[s].push_back(held);
Ok(())
}
pub fn take_blob_jobs(&mut self) -> Vec<BlobJob> {
self.blobs
.as_mut()
.map(|b| b.jobs.drain(..).collect())
.unwrap_or_default()
}
pub fn has_blob_jobs(&self) -> bool {
self.blobs.as_ref().is_some_and(|b| !b.jobs.is_empty())
}
pub fn blobs_written(&mut self, through: u64) -> io::Result<()> {
let Some(area) = self.blobs.as_mut() else {
return Ok(());
};
area.written = area.written.max(through);
let written = area.written;
let mut ready: Vec<(u32, Held)> = Vec::new();
for (s, q) in area.held.iter_mut().enumerate() {
while q.front().is_some_and(|h| h.needs <= written) {
ready.push((s as u32, q.pop_front().expect("front exists")));
}
}
for (s, h) in ready {
self.stage_plain(s, h.batch, h.files)?;
}
Ok(())
}
pub fn flush_blobs(&mut self) -> io::Result<()> {
let jobs = self.take_blob_jobs();
let Some(last) = jobs.last().map(|j| j.id) else {
return Ok(());
};
let dev = &self.blobs.as_ref().expect("jobs come from a blob area").dev;
let mut files: Vec<u64> = Vec::new();
for j in &jobs {
j.perform(dev)?;
if !files.contains(&j.file) {
files.push(j.file);
}
}
for f in files {
dev.sync(f)?;
}
self.blobs_written(last)
}
pub fn blob_device(&self) -> Option<&D> {
self.blobs.as_ref().map(|b| &b.dev)
}
pub fn blob_stats(&self) -> BlobStats {
self.blobs
.as_ref()
.map_or_else(BlobStats::default, |b| BlobStats {
files: b.files.len(),
records: b.records,
bytes: b.bytes,
held: b.held.iter().map(|q| q.len()).sum(),
jobs: b.jobs.len(),
})
}
fn stage_plain(&mut self, shard: u32, batch: Batch, refs: Vec<u64>) -> io::Result<()> {
let slot = self
.staged
.get_mut(shard as usize)
.ok_or_else(|| io_other(format!("no shard {shard}")))?;
self.staged_refs[shard as usize].extend(refs);
match slot {
Some(s) => {
if batch.first_lsn != s.last_lsn + 1 {
return Err(io_other("staged sections are not consecutive"));
}
s.section.extend_from_slice(&batch.section);
s.last_lsn = batch.last_lsn;
s.records += batch.records;
s.bytes += batch.bytes;
}
None => *slot = Some(batch),
}
Ok(())
}
pub fn has_staged(&self) -> bool {
self.staged.iter().any(Option::is_some)
}
pub fn staged_shards(&self) -> impl Iterator<Item = u32> + '_ {
self.staged
.iter()
.enumerate()
.filter(|(_, s)| s.is_some())
.map(|(i, _)| i as u32)
}
pub fn write(&mut self, now_ms: u64) -> io::Result<Vec<u32>> {
if !self.in_flight.is_empty() {
return Err(io_other("a write is still in flight"));
}
let mut shards = Vec::new();
while let Some(w) = self.begin_write(now_ms, false)? {
for s in self.finish_write(w)? {
if !shards.contains(&s) {
shards.push(s);
}
}
}
Ok(shards)
}
pub fn can_begin(&self) -> bool {
self.in_flight.len() < self.opts.in_flight.max(1)
&& self.in_flight.back().is_none_or(|w| w.padded)
}
pub fn in_flight(&self) -> usize {
self.in_flight.len()
}
#[cfg(test)]
fn complete_is_pending(&self) -> bool {
!self.in_flight.is_empty()
}
pub fn begin_write(&mut self, now_ms: u64, overlap: bool) -> io::Result<Option<InFlight>> {
let Some(mut w) = self.seal(now_ms, overlap)? else {
return Ok(None);
};
Self::perform_write(&self.dev, &mut w)?;
Ok(Some(w))
}
fn perform_write(dev: &D, w: &mut InFlight) -> io::Result<()> {
if let Some((header, offset, bytes)) = w.io.take() {
if let Some(h) = header {
dev.write(w.segment, 0, &h)?;
}
dev.write(w.segment, offset, &bytes)?;
}
Ok(())
}
pub fn perform(dev: &D, w: &mut InFlight) -> io::Result<()> {
Self::perform_write(dev, w)?;
dev.sync(w.segment)
}
pub fn seal(&mut self, now_ms: u64, overlap: bool) -> io::Result<Option<InFlight>> {
if !self.can_begin() {
return Ok(None);
}
let pipelined = overlap && self.opts.in_flight > 1;
let shards = self.staged.len();
let mut room = self.batch_limit.saturating_sub(BATCH_HEADER);
let mut staged: Vec<(u32, Batch)> = Vec::new();
let mut cut: Option<(u32, usize)> = None;
let mut next_from = 0;
for i in (0..shards).map(|k| (self.seal_from + k) % shards) {
let Some(b) = self.staged[i].as_mut() else {
continue;
};
let need = SECTION_HEADER + b.section.len();
if need <= room {
room -= need;
if let Some(b) = self.staged[i].take() {
staged.push((i as u32, b));
}
continue;
}
match split_section(b, room.saturating_sub(SECTION_HEADER))? {
Some((head, refs)) => {
staged.push((i as u32, head));
cut = Some((i as u32, refs));
next_from = (i + 1) % shards;
}
None if !staged.is_empty() => next_from = i,
None => {
return Err(io_other(format!(
"shard {i} staged a record longer than a log batch ({} bytes)",
self.batch_limit
)));
}
}
break;
}
if staged.is_empty() {
return Ok(None);
}
self.seal_from = next_from;
staged.sort_unstable_by_key(|(shard, _)| *shard);
let sections: Vec<SectionRef<'_>> = staged
.iter()
.map(|(shard, b)| SectionRef {
shard: *shard,
first_lsn: b.first_lsn,
count: b.records,
records: &b.section,
})
.collect();
let body = batch_body_len(§ions);
let size = batch_span(self.writer.offset(), body, pipelined);
let next = if self.writer.offset() + size > self.current_len() {
Some(self.next_segment(batch_span(WAL_HEADER_BYTES, body, pipelined))?)
} else {
None
};
let placed = self
.writer
.place(§ions, self.acked, pipelined, now_ms, || {
next.expect("a next segment was prepared")
});
debug_assert!(placed.bytes.len() <= MAX_BATCH, "recovery reads this batch");
self.writes += 1;
self.rolled_at.2 = now_ms;
if let Some(h) = &placed.header {
self.rolls += 1;
self.rolled_at = (now_ms, self.bytes_written, now_ms);
self.live.push(LiveSegment {
number: placed.segment,
base_seq: h.base_seq,
len: next.map_or(0, |n| n.2),
});
}
self.bytes_written += placed.bytes.len() as u64;
self.prepare_ahead()?;
self.in_flight.push_back(Pending {
seq: placed.seq,
padded: pipelined && (placed.offset + placed.bytes.len() as u64).is_multiple_of(BLOCK),
done: None,
});
Ok(Some(InFlight {
segment: placed.segment,
seq: placed.seq,
io: Some((
placed.header.as_ref().map(WalHeader::encode),
placed.offset,
placed.bytes,
)),
shards: {
for (s, b) in &staged {
self.holding[*s as usize].push_back((placed.seq, b.last_lsn));
let all = &mut self.staged_refs[*s as usize];
let refs: Vec<u64> = match cut {
Some((shard, n)) if shard == *s => all.drain(..n.min(all.len())).collect(),
_ => std::mem::take(all),
};
if let Some(area) = self.blobs.as_mut() {
for f in refs {
if let Some(file) = area.files.get_mut(&f) {
file.last_ref = placed.seq;
file.pending = file.pending.saturating_sub(1);
}
}
}
}
staged.iter().map(|(s, b)| (*s, b.last_lsn)).collect()
},
}))
}
pub fn finish_write(&mut self, w: InFlight) -> io::Result<Vec<u32>> {
self.dev.sync(w.segment)?;
Ok(self.complete(w))
}
pub fn complete(&mut self, w: InFlight) -> Vec<u32> {
if let Some(p) = self.in_flight.iter_mut().find(|p| p.seq == w.seq) {
p.done = Some(w.shards);
}
let mut shards = Vec::new();
while self.in_flight.front().is_some_and(|p| p.done.is_some()) {
let p = self.in_flight.pop_front().expect("front exists");
self.acked = p.seq;
for (shard, last) in p.done.unwrap_or_default() {
self.durable[shard as usize] = last;
if !shards.contains(&shard) {
shards.push(shard);
}
}
}
shards
}
fn current_len(&self) -> u64 {
self.live.last().map_or(0, |s| s.len)
}
fn next_size(&self) -> u64 {
(self.current_len().max(self.opts.first_segment_bytes) * 2).min(self.largest_segment())
}
fn largest_segment(&self) -> u64 {
const FAST_BYTES_PER_MS: u64 = 100_000;
const FAST_SEGMENT: u64 = 256 << 20;
let max = self.opts.segment_bytes;
if !(fusee_core::segment::DEFAULT_SEGMENT_BYTES..FAST_SEGMENT).contains(&max) {
return max;
}
let written = self.bytes_written.saturating_sub(self.rolled_at.1);
let ms = self.rolled_at.2.saturating_sub(self.rolled_at.0).max(1);
if written >= 4 << 20 && written / ms > FAST_BYTES_PER_MS {
FAST_SEGMENT
} else {
max
}
}
fn prepare_ahead(&mut self) -> io::Result<()> {
if self.prepared.is_some() || self.writer.offset() < self.current_len() / 4 {
return Ok(());
}
let size = self.next_size();
if !self.spares.is_empty() {
let active = self.live.last().map(|s| s.number);
if self.spares_fit == active {
return Ok(());
}
let mut fits = false;
for &spare in &self.spares {
fits |= self.dev.len(spare)? >= size;
}
if fits {
self.spares_fit = active;
return Ok(());
}
}
let number = self.next_number;
self.next_number += 1;
self.dev.prepare(number, size)?;
self.prepared = Some((number, size));
Ok(())
}
fn next_segment(&mut self, need: u64) -> io::Result<(u64, u64, u64)> {
let grown = self.next_size().max(need + WAL_HEADER_BYTES);
if let Some((number, size)) = self.prepared.take() {
if size < need + WAL_HEADER_BYTES {
self.dev.remove(number)?;
} else if self.dev.is_ready(number) || self.spares.is_empty() {
self.rng = xorshift(self.rng);
return Ok((number, self.rng, size));
} else {
self.prepared = Some((number, size));
}
}
let number = self.next_number;
self.next_number += 1;
let mut size = 0;
let mut lens = Vec::with_capacity(self.spares.len());
for &spare in &self.spares {
lens.push((self.dev.len(spare)?, spare));
}
lens.sort_unstable();
self.spares = lens.iter().map(|&(_, n)| n).collect();
while let Some(spare) = self.spares.pop() {
let len = self.dev.len(spare)?;
if len >= need + WAL_HEADER_BYTES {
self.dev.rename(spare, number)?;
size = len;
break;
}
self.dev.remove(spare)?;
}
if size == 0
&& let Some((prepared, len)) = self.prepared.take()
{
self.next_number -= 1;
self.rng = xorshift(self.rng);
return Ok((prepared, self.rng, len));
}
if size == 0 {
self.dev.create(number, grown)?;
size = grown;
}
self.rng = xorshift(self.rng);
Ok((number, self.rng, size))
}
pub fn durable(&self, shard: u32) -> Lsn {
self.durable.get(shard as usize).copied().unwrap_or(0)
}
pub fn position(&self) -> u64 {
self.writer.next_seq()
}
pub fn active_segment(&self) -> u64 {
self.writer.segment()
}
fn needed(&self, shard: usize) -> Option<u64> {
match self.retain_lsn[shard] {
None => Some(0),
Some(_) => self.holding[shard].front().map(|(seq, _)| *seq),
}
}
pub fn pins(&self, shard: u32) -> bool {
let Some(mine) = self.needed(shard as usize) else {
return false;
};
let min = (0..self.holding.len())
.filter_map(|s| self.needed(s))
.min()
.unwrap_or(u64::MAX);
mine == min
&& self.backup_needs.is_none_or(|b| b > mine)
&& self.live.len() >= 2
&& self.live[1].base_seq > mine
&& self.live.last().is_some_and(|s| mine < s.base_seq)
}
pub fn holds_retiring(&self, shard: u32) -> bool {
let Some(mine) = self.needed(shard as usize) else {
return false;
};
self.live.len() >= 2 && mine < self.live[1].base_seq
}
pub fn holds_old(&self, shard: u32) -> bool {
let Some(mine) = self.needed(shard as usize) else {
return false;
};
self.live.len() >= 2 && self.live.last().is_some_and(|s| mine < s.base_seq)
}
pub fn set_retain(&mut self, shard: u32, lsn: Lsn) -> io::Result<()> {
let s = shard as usize;
if s >= self.holding.len() {
return Ok(());
}
self.retain_lsn[s] = Some(lsn);
while self.holding[s]
.front()
.is_some_and(|(_, last)| *last <= lsn)
{
self.holding[s].pop_front();
}
self.retire()
}
pub fn set_backup(&mut self, needs: Option<u64>) -> io::Result<()> {
self.backup_needs = needs;
self.retire()
}
fn retire(&mut self) -> io::Result<()> {
let min = (0..self.holding.len())
.filter_map(|s| self.needed(s))
.min()
.unwrap_or(u64::MAX)
.min(self.backup_needs.unwrap_or(u64::MAX))
.min(self.position());
while self.live.len() >= 2 && self.live[1].base_seq <= min {
let old = self.live.remove(0);
self.dev
.write(old.number, 0, &vec![0; WAL_HEADER_BYTES as usize])?;
self.dev.sync(old.number)?;
if self.spares.len() < self.opts.spares {
self.spares.push(old.number);
} else {
self.dev.remove(old.number)?;
}
}
if let (Some(area), Some(first)) = (self.blobs.as_mut(), self.live.first()) {
area.retire(first.base_seq)?;
}
Ok(())
}
pub fn shorten(&mut self, now_ms: u64) -> io::Result<bool> {
let quiet = self.in_flight.is_empty()
&& !self.has_staged()
&& self
.blobs
.as_ref()
.is_none_or(|a| a.jobs.is_empty() && a.held.iter().all(|h| h.is_empty()));
let unneeded = self.backup_needs.is_none()
&& (0..self.holding.len()).all(|s| self.needed(s).is_none());
if !quiet || !unneeded || self.writer.offset() <= WAL_HEADER_BYTES {
return Ok(false);
}
let (number, salt, len) = self.empty_segment()?;
let header = self.writer.roll(number, salt, len, now_ms);
self.dev.write(number, 0, &header.encode())?;
self.dev.sync(number)?;
self.rolls += 1;
self.rolled_at = (now_ms, self.bytes_written, now_ms);
self.live.push(LiveSegment {
number,
base_seq: header.base_seq,
len,
});
self.retire()?;
Ok(true)
}
fn empty_segment(&mut self) -> io::Result<(u64, u64, u64)> {
self.rng = xorshift(self.rng);
if let Some((number, len)) = self.prepared
&& self.dev.is_ready(number)
{
self.prepared = None;
return Ok((number, self.rng, len));
}
let number = self.next_number;
self.next_number += 1;
let mut largest: Option<(u64, usize)> = None;
for (i, &spare) in self.spares.iter().enumerate() {
let len = self.dev.len(spare)?;
if largest.is_none_or(|(l, _)| len > l) {
largest = Some((len, i));
}
}
if let Some((len, i)) = largest.filter(|(len, _)| *len > WAL_HEADER_BYTES) {
let spare = self.spares.remove(i);
self.dev.rename(spare, number)?;
return Ok((number, self.rng, len));
}
let len = self.opts.first_segment_bytes.min(self.opts.segment_bytes);
self.dev.create(number, len)?;
Ok((number, self.rng, len))
}
pub fn files(&self) -> (Vec<u64>, Vec<u64>) {
(
self.live.iter().map(|s| s.number).collect(),
self.spares.clone(),
)
}
pub fn device(&self) -> &D {
&self.dev
}
pub fn discard_staged(&mut self, shard: u32) -> bool {
let mut any = false;
if let Some(refs) = self.staged_refs.get_mut(shard as usize) {
let refs = std::mem::take(refs);
if let Some(area) = self.blobs.as_mut() {
for f in refs {
if let Some(file) = area.files.get_mut(&f) {
file.pending = file.pending.saturating_sub(1);
}
}
}
}
if let Some(q) = self
.blobs
.as_mut()
.and_then(|a| a.held.get_mut(shard as usize))
{
any = !q.is_empty();
let dropped: Vec<Held> = q.drain(..).collect();
let area = self.blobs.as_mut().expect("held above");
for h in dropped {
for f in h.files {
if let Some(file) = area.files.get_mut(&f) {
file.pending = file.pending.saturating_sub(1);
}
}
}
}
self.staged
.get_mut(shard as usize)
.and_then(Option::take)
.is_some()
|| any
}
pub fn chain(&self) -> Chain {
Chain {
segments: self
.live
.iter()
.map(|s| (s.number, s.len, s.base_seq))
.collect(),
acked: self.acked,
}
}
pub fn acked(&self) -> u64 {
self.acked
}
}
fn resolve_records<D: LogDevice>(
s: &SectionRef<'_>,
after: Lsn,
blobs: Option<&D>,
) -> io::Result<Vec<(Lsn, RecordBuf)>> {
let mut fetch = |r: &OffloadRef| -> io::Result<Vec<u8>> {
match blobs {
Some(dev) => dev.read(r.file, r.offset, r.len as usize),
None => Err(io_other("this log has no blob area")),
}
};
section_records_resolved(s, after, &mut fetch).map_err(|e| match e {
fusee_core::wal::ResolveError::Blob { .. } => io_other(format!(
"shard {} {e}: an acknowledged record was lost or damaged. Restore from a replica \
or backup.",
s.shard
)),
e => io_other(format!("shard {}: {e}", s.shard)),
})
}
pub fn resolve_section<D: LogDevice>(s: &SectionRef<'_>, blobs: Option<&D>) -> io::Result<Vec<u8>> {
let mut out = Vec::with_capacity(s.records.len());
let mut any = false;
for (i, rec) in fusee_core::wal::section_spans(s.records).enumerate() {
let rec = rec.map_err(io_other)?;
if fusee_core::wal::is_offloaded(rec.body) {
any = true;
let r = OffloadRef::decode(rec.body).map_err(io_other)?;
let lsn = s.first_lsn + i as u64;
let bytes = match blobs {
Some(dev) => dev.read(r.file, r.offset, r.len as usize)?,
None => Vec::new(),
};
if !r.matches(&bytes) {
return Err(io_other(format!(
"shard {} record {lsn} references bytes {}..{} of blob file {}, which are \
missing or damaged",
s.shard,
r.offset,
r.offset + r.len,
r.file
)));
}
fusee_core::wire::put_bytes(&mut out, &bytes);
} else {
fusee_core::wire::put_bytes(&mut out, rec.body);
}
}
if !any {
out.clear();
out.extend_from_slice(s.records);
}
Ok(out)
}
#[derive(Clone, Debug, Default)]
pub struct Chain {
segments: Vec<(u64, u64, u64)>,
acked: u64,
}
impl Chain {
pub fn acked(&self) -> u64 {
self.acked
}
pub fn segments(&self) -> &[(u64, u64, u64)] {
&self.segments
}
}
pub type RawSection = (Lsn, u32, Vec<u8>);
pub fn read_sections<D: LogDevice>(
dev: &D,
chain: &Chain,
shards: &[u32],
) -> io::Result<Vec<Vec<RawSection>>> {
let mut out: Vec<Vec<RawSection>> = shards.iter().map(|_| Vec::new()).collect();
if chain.acked == 0 {
return Ok(out);
}
let mut expect: Option<(u64, u64)> = None;
for &(number, len, base_seq) in &chain.segments {
if base_seq > chain.acked {
break;
}
let mut chunk = 256 << 10;
let mut bytes = dev.read(number, 0, chunk)?;
let Some(mut scanner) =
WalScanner::start(&bytes, bytes.len() as u64 >= len).map_err(io_other)?
else {
return Err(io_other(format!(
"log segment {number} lost its header while it was being read"
)));
};
let h = *scanner.header();
if h.segment != number
|| h.base_seq != base_seq
|| expect.is_some_and(|(seq, sum)| h.base_seq != seq + 1 || h.base_prev != sum)
{
return Err(io_other(format!(
"log segment {number} does not continue the chain"
)));
}
let mut reached = false;
let scanned = loop {
let complete = bytes.len() as u64 >= len;
let progress = scanner.advance(&bytes, complete, len as usize, &mut |b| {
if reached || b.header.seq > chain.acked {
return;
}
for s in b.sections() {
if let Some(i) = shards.iter().position(|x| *x == s.shard) {
out[i].push((s.first_lsn, s.count, s.records.to_vec()));
}
}
reached = b.header.seq == chain.acked;
});
if reached {
return Ok(out);
}
match progress {
Progress::Done(s) => break s,
Progress::NeedMore => {
chunk = (chunk * 2).min(16 << 20);
let more = dev.read(number, bytes.len() as u64, chunk)?;
if more.is_empty() {
return Err(io_other(format!(
"reading log segment {number} made no progress"
)));
}
bytes.extend_from_slice(&more);
}
}
};
expect = Some((scanned.last_seq, scanned.last_checksum));
}
Err(io_other(format!(
"the log ends at batch {} before durable batch {}",
expect.map_or(0, |e| e.0),
chain.acked
)))
}
pub fn records_after(
shard: u32,
sections: &[RawSection],
after: Lsn,
) -> io::Result<(Vec<(Lsn, RecordBuf)>, Lsn)> {
let mut out = Vec::new();
let mut expected = after + 1;
let mut last = after;
for (first, count, bytes) in sections {
let end = first + u64::from(*count) - 1;
if end <= after {
continue;
}
if *first > expected {
return Err(io_other(format!(
"shard {shard} records {expected}..{} are missing from the log",
first - 1
)));
}
let s = SectionRef {
shard,
first_lsn: *first,
count: *count,
records: bytes,
};
for (lsn, rec) in fusee_core::wal::section_records(&s).map_err(io_other)? {
if lsn > after {
out.push((lsn, rec));
}
}
expected = end + 1;
last = end;
}
Ok((out, last))
}
fn xorshift(mut x: u64) -> u64 {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x
}
pub fn new_life() -> u64 {
let t = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let mut x = t ^ (u64::from(std::process::id()) << 32) ^ (&t as *const u64 as u64);
for _ in 0..4 {
x = xorshift(x | 1);
}
x
}
pub fn encode_sections(out: &mut Vec<u8>, sections: &[SectionRef<'_>]) {
out.reserve(
4 + sections
.iter()
.map(|s| fusee_core::wal::SECTION_HEADER + s.records.len())
.sum::<usize>(),
);
out.extend_from_slice(&(sections.len() as u32).to_le_bytes());
for s in sections {
out.extend_from_slice(&s.shard.to_le_bytes());
out.extend_from_slice(&s.count.to_le_bytes());
out.extend_from_slice(&s.first_lsn.to_le_bytes());
out.extend_from_slice(&(s.records.len() as u32).to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(s.records);
}
}
pub fn decode_sections(bytes: &[u8]) -> io::Result<Vec<SectionRef<'_>>> {
let bad = || io_other("malformed log sections");
let n = u32::from_le_bytes(bytes.get(..4).ok_or_else(bad)?.try_into().unwrap());
let mut out = Vec::with_capacity((n as usize).min(1024));
let mut at = 4usize;
for _ in 0..n {
let h = bytes
.get(at..at + fusee_core::wal::SECTION_HEADER)
.ok_or_else(bad)?;
let u32_at = |i: usize| u32::from_le_bytes(h[i..i + 4].try_into().unwrap());
let len = u32_at(16) as usize;
let start = at + fusee_core::wal::SECTION_HEADER;
let records = bytes.get(start..start + len).ok_or_else(bad)?;
let s = SectionRef {
shard: u32_at(0),
count: u32_at(4),
first_lsn: u64::from_le_bytes(h[8..16].try_into().unwrap()),
records,
};
if s.count == 0 || s.first_lsn == 0 {
return Err(bad());
}
out.push(s);
at = start + len;
}
if at != bytes.len() {
return Err(bad());
}
Ok(out)
}
impl<T: LogDevice> LogDevice for Arc<T> {
fn segments(&self) -> io::Result<Vec<u64>> {
(**self).segments()
}
fn len(&self, segment: u64) -> io::Result<u64> {
(**self).len(segment)
}
fn read(&self, segment: u64, offset: u64, len: usize) -> io::Result<Vec<u8>> {
(**self).read(segment, offset, len)
}
fn create(&self, segment: u64, len: u64) -> io::Result<()> {
(**self).create(segment, len)
}
fn write(&self, segment: u64, offset: u64, bytes: &[u8]) -> io::Result<()> {
(**self).write(segment, offset, bytes)
}
fn sync(&self, segment: u64) -> io::Result<()> {
(**self).sync(segment)
}
fn rename(&self, from: u64, to: u64) -> io::Result<()> {
(**self).rename(from, to)
}
fn remove(&self, segment: u64) -> io::Result<()> {
(**self).remove(segment)
}
fn prepare(&self, segment: u64, len: u64) -> io::Result<()> {
(**self).prepare(segment, len)
}
fn is_ready(&self, segment: u64) -> bool {
(**self).is_ready(segment)
}
}
struct ShardSlot {
durable: AtomicU64,
notify: Mutex<Option<Notify>>,
submitted: AtomicU64,
waiting: AtomicU64,
waiter: Mutex<Option<std::thread::Thread>>,
}
enum Wake {
Thread(std::thread::Thread),
Notify(Notify),
}
impl Wake {
fn run(self) {
match self {
Wake::Thread(t) => t.unpark(),
Wake::Notify(n) => n(),
}
}
}
const NOBODY: u64 = u64::MAX;
const PAD_FOR_COMPANY_US: u64 = 10;
#[derive(Default)]
struct Woken {
kicked: bool,
waiting: bool,
}
#[derive(Default)]
struct Jobs {
queue: std::collections::VecDeque<(InFlight, Instant)>,
idle: usize,
}
const HANDOFF_STALE_NS: u64 = 1_000_000;
pub struct SharedWal {
wake: Arc<Condvar>,
woken: Arc<Mutex<Woken>>,
kicked: AtomicBool,
direct: AtomicBool,
queued: AtomicU64,
spin_cap_us: u64,
stop: Arc<AtomicBool>,
threads: Mutex<Vec<std::thread::JoinHandle<()>>>,
dev: Arc<FileDevice>,
wal: Mutex<Wal<Arc<FileDevice>>>,
jobs: Arc<Mutex<Jobs>>,
job_ready: Arc<Condvar>,
writes: AtomicU64,
handed: AtomicU64,
handed_at: AtomicU64,
staging: Mutex<Vec<(u32, Batch, Vec<Large>)>>,
blob_dev: Arc<FileDevice>,
offload: u64,
blob_ready: Arc<Condvar>,
blob_kicked: Arc<Mutex<bool>>,
shards: Vec<ShardSlot>,
failed: Mutex<Option<String>>,
has_failed: AtomicBool,
last_flush_us: AtomicU64,
last_batch_shards: AtomicU64,
in_flight_limit: usize,
owner: bool,
owner_fd: i32,
#[cfg(all(target_os = "linux", feature = "io-uring"))]
#[allow(dead_code)]
owner_efd: Option<Arc<std::os::fd::OwnedFd>>,
#[cfg_attr(not(all(target_os = "linux", feature = "io-uring")), allow(dead_code))]
owner_waiting: AtomicBool,
stats: [AtomicU64; 3],
position: AtomicU64,
segment: AtomicU64,
backup: Mutex<Option<Arc<crate::backup::Shipper>>>,
}
impl SharedWal {
pub fn open(
dir: &Path,
shards: u32,
local: LocalOptions,
opts: WalOptions,
) -> io::Result<Arc<Self>> {
Self::open_with_offload(dir, shards, local, opts, 0)
}
pub fn open_with_offload(
dir: &Path,
shards: u32,
local: LocalOptions,
opts: WalOptions,
offload: u64,
) -> io::Result<Arc<Self>> {
let blob_dev = Arc::new(FileDevice::blobs(&dir.join("blobs"), local.clone()));
#[cfg(all(target_os = "linux", feature = "io-uring"))]
let local_for_owner = local.clone();
let dev = Arc::new(FileDevice::open(dir, local)?);
let wal = Wal::open_with_blobs(
dev.clone(),
Blobs {
dev: blob_dev.clone(),
threshold: offload,
},
shards,
opts,
new_life(),
unix_ms(),
)?;
let position = wal.position();
let segment = wal.active_segment();
let slots = (0..shards)
.map(|s| ShardSlot {
durable: AtomicU64::new(wal.durable(s)),
notify: Mutex::new(None),
submitted: AtomicU64::new(0),
waiting: AtomicU64::new(0),
waiter: Mutex::new(None),
})
.collect();
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
#[cfg(all(target_os = "linux", feature = "io-uring"))]
let (owner_ring, owner_efd) = match owner::wanted(&dev, &local_for_owner) {
Some((ring, efd)) => (Some(ring), Some(Arc::new(efd))),
None => (None, None),
};
#[cfg(all(target_os = "linux", feature = "io-uring"))]
let owner_fd = owner_efd.as_ref().map_or(-1, |f| {
use std::os::fd::AsRawFd;
f.as_raw_fd()
});
#[cfg(not(all(target_os = "linux", feature = "io-uring")))]
let owner_fd = -1;
let shared = Arc::new(Self {
wake: Arc::new(Condvar::new()),
woken: Arc::new(Mutex::new(Woken::default())),
kicked: AtomicBool::new(false),
direct: AtomicBool::new(false),
queued: AtomicU64::new(0),
spin_cap_us: if cores > 2 && !crate::spin::spin_off() {
50
} else {
0
},
stop: Arc::new(AtomicBool::new(false)),
threads: Mutex::new(Vec::new()),
dev,
wal: Mutex::new(wal),
jobs: Arc::new(Mutex::new(Jobs::default())),
job_ready: Arc::new(Condvar::new()),
writes: AtomicU64::new(0),
handed: AtomicU64::new(NOBODY),
handed_at: AtomicU64::new(0),
staging: Mutex::new(Vec::new()),
blob_dev,
offload,
blob_ready: Arc::new(Condvar::new()),
blob_kicked: Arc::new(Mutex::new(false)),
shards: slots,
failed: Mutex::new(None),
has_failed: AtomicBool::new(false),
last_flush_us: AtomicU64::new(0),
last_batch_shards: AtomicU64::new(0),
in_flight_limit: opts.in_flight.max(1),
stats: [const { AtomicU64::new(0) }; 3],
owner: owner_fd >= 0,
owner_fd,
#[cfg(all(target_os = "linux", feature = "io-uring"))]
owner_efd: owner_efd.clone(),
owner_waiting: AtomicBool::new(false),
position: AtomicU64::new(position),
segment: AtomicU64::new(segment),
backup: Mutex::new(None),
});
let mut threads = Vec::new();
#[cfg(all(target_os = "linux", feature = "io-uring"))]
if let (Some(ring), Some(efd)) = (owner_ring, owner_efd) {
let weak = Arc::downgrade(&shared);
threads.push(
std::thread::Builder::new()
.name("fusee-wal-owner".into())
.spawn(move || owner::run(weak, ring, efd))?,
);
}
if !shared.owner {
let weak = Arc::downgrade(&shared);
threads.push(
std::thread::Builder::new()
.name("fusee-wal".into())
.spawn(move || log_thread(weak))?,
);
for i in 0..opts.in_flight.max(1) {
let weak = Arc::downgrade(&shared);
threads.push(
std::thread::Builder::new()
.name(format!("fusee-wal-io-{i}"))
.spawn(move || io_thread(weak))?,
);
}
}
if offload > 0 {
let weak = Arc::downgrade(&shared);
threads.push(
std::thread::Builder::new()
.name("fusee-wal-blob".into())
.spawn(move || blob_thread(weak))?,
);
}
*shared.threads.lock().unwrap_or_else(|e| e.into_inner()) = threads;
Ok(shared)
}
fn kick(&self) {
#[cfg(all(target_os = "linux", feature = "io-uring"))]
if self.owner {
self.kicked.store(true, Ordering::SeqCst);
owner::wake(self);
return;
}
let waiting = {
let mut w = self.woken.lock().unwrap_or_else(|e| e.into_inner());
w.kicked = true;
w.waiting
};
self.kicked.store(true, Ordering::Release);
if waiting {
self.wake.notify_one();
}
}
pub fn set_direct_submit(&self, on: bool) {
self.direct.store(on, Ordering::Relaxed);
}
fn spin_budget(&self) -> std::time::Duration {
if self.last_batch_shards.load(Ordering::Relaxed) > 1
|| !crate::spin::few_busy()
|| !crate::spin::spare_core()
{
return std::time::Duration::ZERO;
}
let flush = self.last_flush_us.load(Ordering::Relaxed);
std::time::Duration::from_micros(self.spin_cap_us.min(flush))
}
fn spin(&self, ready: impl Fn() -> bool) -> bool {
let budget = self.spin_budget();
if budget.is_zero() {
return ready();
}
let until = Instant::now() + budget;
loop {
if ready() {
return true;
}
if Instant::now() >= until {
return false;
}
std::hint::spin_loop();
}
}
fn fail_message(&self) -> Option<String> {
if !self.has_failed.load(Ordering::Acquire) {
return None;
}
self.failed
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
fn fail(&self, e: io::Error) {
*self.failed.lock().unwrap_or_else(|e| e.into_inner()) =
Some(format!("log write failed: {e}"));
self.has_failed.store(true, Ordering::Release);
for slot in &self.shards {
if let Some(t) = slot
.waiter
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
{
t.unpark();
}
if let Some(n) = slot
.notify
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
{
n();
}
}
}
fn stage(&self, shard: u32, batch: Batch) {
let large = if self.offload > 0 && batch.bytes > self.offload {
match find_large(&batch.section, self.offload as usize) {
Ok(l) => l,
Err(e) => return self.fail(io_other(e)),
}
} else {
Vec::new()
};
self.staging
.lock()
.unwrap_or_else(|e| e.into_inner())
.push((shard, batch, large));
}
fn take_staged(&self, wal: &mut Wal<Arc<FileDevice>>) -> io::Result<()> {
let staged = std::mem::take(&mut *self.staging.lock().unwrap_or_else(|e| e.into_inner()));
for (shard, b, large) in staged {
wal.stage_found(shard, b, large)?;
}
if wal.has_blob_jobs() {
*self.blob_kicked.lock().unwrap_or_else(|e| e.into_inner()) = true;
self.blob_ready.notify_one();
}
Ok(())
}
#[cfg(all(test, target_os = "linux"))]
pub(crate) fn reject_direct_writes(&self) {
self.dev.reject_direct.store(true, Ordering::Relaxed);
}
pub fn direct_io(&self) -> bool {
self.dev.direct_io()
}
pub fn blob_stats(&self) -> BlobStats {
self.wal
.lock()
.unwrap_or_else(|e| e.into_inner())
.blob_stats()
}
fn publish(
&self,
wal: &Wal<Arc<FileDevice>>,
shards: &[u32],
me: Option<u32>,
wakes: &mut Vec<Wake>,
) {
self.position.store(wal.position(), Ordering::Release);
self.segment.store(wal.active_segment(), Ordering::Release);
self.writes.store(wal.in_flight() as u64, Ordering::SeqCst);
for (a, v) in self
.stats
.iter()
.zip([wal.writes, wal.bytes_written, wal.rolls])
{
a.store(v, Ordering::Relaxed);
}
for &s in shards {
let slot = &self.shards[s as usize];
let durable = wal.durable(s);
slot.durable.store(durable, Ordering::Release);
if me == Some(s) {
continue;
}
let waiting = slot.waiting.load(Ordering::Acquire);
if waiting != 0
&& durable >= waiting
&& let Some(t) = slot
.waiter
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
{
wakes.push(Wake::Thread(t.clone()));
}
loop {
let want = slot.submitted.load(Ordering::Acquire);
if want == 0 {
break;
}
if durable < want
|| slot
.submitted
.compare_exchange(want, 0, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
if let Some(n) = slot
.notify
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
{
wakes.push(Wake::Notify(n.clone()));
}
break;
}
}
}
}
fn next_write(&self, wal: &mut Wal<Arc<FileDevice>>, me: Option<u32>, wakes: &mut Vec<Wake>) {
if self.has_failed.load(Ordering::Acquire) || self.handed.load(Ordering::Acquire) != NOBODY
{
return;
}
if let Err(e) = self.take_staged(wal) {
return self.fail(e);
}
if !wal.has_staged() {
return;
}
if wal.in_flight() == 0 {
let lead = wal.staged_shards().find(|&s| {
Some(s) != me && self.shards[s as usize].waiting.load(Ordering::Acquire) != 0
});
if let Some(s) = lead
&& let Some(t) = self.shards[s as usize]
.waiter
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
{
self.handed_at
.store(crate::spin::clock_ns(), Ordering::Release);
self.handed.store(u64::from(s), Ordering::Release);
wakes.push(Wake::Thread(t.clone()));
return;
}
}
self.start_writes(wal);
}
fn start_writes(&self, wal: &mut Wal<Arc<FileDevice>>) {
if self.has_failed.load(Ordering::Acquire) || self.handed.load(Ordering::Acquire) != NOBODY
{
return;
}
if let Err(e) = self.take_staged(wal) {
return self.fail(e);
}
while wal.has_staged() && wal.can_begin() {
match wal.seal(unix_ms(), true) {
Ok(Some(w)) => {
self.writes.store(wal.in_flight() as u64, Ordering::SeqCst);
self.last_batch_shards
.store(w.shards.len() as u64, Ordering::Relaxed);
let idle = {
let mut j = self.jobs.lock().unwrap_or_else(|e| e.into_inner());
j.queue.push_back((w, Instant::now()));
j.idle
};
self.queued.fetch_add(1, Ordering::SeqCst);
#[cfg(all(target_os = "linux", feature = "io-uring"))]
if self.owner {
owner::wake(self);
continue;
}
if idle > 0 {
self.job_ready.notify_one();
}
}
Ok(None) => break,
Err(e) => return self.fail(e),
}
}
}
fn submit(&self, shard: u32, batch: Batch) {
self.shards[shard as usize]
.submitted
.store(batch.last_lsn, Ordering::Release);
self.stage(shard, batch);
if self.direct.load(Ordering::Relaxed) {
let mut wal = self.wal.lock().unwrap_or_else(|e| e.into_inner());
self.start_writes(&mut wal);
} else {
let writes = self.writes.load(Ordering::SeqCst);
if writes == 0 || (log_tuning().second_now && writes < self.in_flight_limit as u64) {
self.kick();
}
}
}
fn write_now(&self, shard: u32, batch: Batch) -> io::Result<Lsn> {
let last = batch.last_lsn;
let slot = &self.shards[shard as usize];
{
let mut w = slot.waiter.lock().unwrap_or_else(|e| e.into_inner());
let me = std::thread::current();
if w.as_ref().is_none_or(|t| t.id() != me.id()) {
*w = Some(me);
}
}
slot.waiting.store(last, Ordering::Release);
self.stage(shard, batch);
let r = self.wait_inline(shard, last, Instant::now());
slot.waiting.store(0, Ordering::Release);
if self.handed.load(Ordering::Acquire) == u64::from(shard) {
let mut wal = self.wal.lock().unwrap_or_else(|e| e.into_inner());
if self.handed.load(Ordering::Acquire) == u64::from(shard) {
self.handed.store(NOBODY, Ordering::Release);
self.start_writes(&mut wal);
}
}
r
}
fn expect_company(&self, me: u32) -> bool {
self.in_flight_limit > 1
&& self.last_flush_us.load(Ordering::Relaxed) >= PAD_FOR_COMPANY_US
&& (log_tuning().pad_always
|| self
.shards
.iter()
.enumerate()
.any(|(i, s)| i != me as usize && s.waiting.load(Ordering::Acquire) != 0))
}
fn wait_inline(&self, shard: u32, last: Lsn, staged_at: Instant) -> io::Result<Lsn> {
let slot = &self.shards[shard as usize];
let mine = u64::from(shard);
loop {
if let Some(m) = self.fail_message() {
return Err(io_other(m));
}
let durable = slot.durable.load(Ordering::Acquire);
if durable >= last {
return Ok(durable);
}
let pending_handoff = {
let wal = self.wal.lock().unwrap_or_else(|e| e.into_inner());
let handed = self.handed.load(Ordering::Acquire);
let stale = handed != NOBODY
&& crate::spin::clock_ns()
.saturating_sub(self.handed_at.load(Ordering::Acquire))
> HANDOFF_STALE_NS;
if wal.in_flight() == 0 && (handed == NOBODY || handed == mine || stale) {
self.handed.store(NOBODY, Ordering::Release);
let overlap = self.expect_company(shard);
self.lead(wal, shard, overlap);
continue;
}
let quarter = std::time::Duration::from_micros(
self.last_flush_us.load(Ordering::Relaxed) / 4,
);
let second = handed == NOBODY
&& wal.can_begin()
&& (wal.has_staged()
|| !self
.staging
.lock()
.unwrap_or_else(|e| e.into_inner())
.is_empty());
if second && (staged_at.elapsed() >= quarter || log_tuning().second_now) {
self.lead(wal, shard, true);
continue;
}
(
handed != NOBODY,
second.then(|| quarter.saturating_sub(staged_at.elapsed())),
)
};
let (pending_handoff, second_in) = pending_handoff;
let ready = || {
slot.durable.load(Ordering::Acquire) >= last
|| self.handed.load(Ordering::Acquire) == mine
|| self.has_failed.load(Ordering::Acquire)
};
if !self.spin(ready) {
let mut wait = if pending_handoff {
std::time::Duration::from_nanos(HANDOFF_STALE_NS)
} else {
std::time::Duration::from_millis(10)
};
if let Some(due) = second_in {
wait = wait.min(due.max(std::time::Duration::from_micros(1)));
}
std::thread::park_timeout(wait);
}
}
}
fn lead(
&self,
mut wal: std::sync::MutexGuard<'_, Wal<Arc<FileDevice>>>,
me: u32,
overlap: bool,
) {
let started = Instant::now();
let sealed = self
.take_staged(&mut wal)
.and_then(|()| wal.seal(unix_ms(), overlap));
let mut w = match sealed {
Ok(Some(w)) => w,
Ok(None) => return,
Err(e) => return self.fail(e),
};
self.writes.store(wal.in_flight() as u64, Ordering::SeqCst);
self.last_batch_shards
.store(w.shards.len() as u64, Ordering::Relaxed);
drop(wal);
let r = Wal::perform(&self.dev, &mut w);
self.last_flush_us
.store(started.elapsed().as_micros() as u64, Ordering::Relaxed);
let mut wakes = Vec::new();
{
let mut wal = self.wal.lock().unwrap_or_else(|e| e.into_inner());
match r {
Ok(()) => {
let shards = wal.complete(w);
self.publish(&wal, &shards, Some(me), &mut wakes);
self.next_write(&mut wal, Some(me), &mut wakes);
}
Err(e) => self.fail(e),
}
}
for w in wakes {
w.run();
}
}
fn take_recovered(&self, shard: u32, after: Lsn) -> io::Result<(Vec<(Lsn, RecordBuf)>, Lsn)> {
let mut wal = self.wal.lock().unwrap_or_else(|e| e.into_inner());
let r = wal.take_recovered(shard, after)?;
self.shards[shard as usize]
.durable
.store(wal.durable(shard), Ordering::Release);
Ok(r)
}
fn set_retain(&self, shard: u32, lsn: Lsn) -> io::Result<()> {
self.wal
.lock()
.unwrap_or_else(|e| e.into_inner())
.set_retain(shard, lsn)
}
fn pins(&self, shard: u32) -> bool {
self.wal
.lock()
.unwrap_or_else(|e| e.into_inner())
.pins(shard)
}
fn holds_old(&self, shard: u32) -> bool {
self.wal
.lock()
.unwrap_or_else(|e| e.into_inner())
.holds_old(shard)
}
pub fn files(&self) -> (Vec<u64>, Vec<u64>) {
self.wal.lock().unwrap_or_else(|e| e.into_inner()).files()
}
pub fn dir(&self) -> &Path {
&self.dev.dir
}
pub(crate) fn backup_snapshot(&self) -> (Chain, u64) {
let wal = self.wal.lock().unwrap_or_else(|e| e.into_inner());
(wal.chain(), unix_ms())
}
pub(crate) fn set_backup_needs(&self, needs: Option<u64>) -> io::Result<()> {
self.wal
.lock()
.unwrap_or_else(|e| e.into_inner())
.set_backup(needs)
}
pub(crate) fn attach_backup(&self, shipper: Arc<crate::backup::Shipper>) {
*self.backup.lock().unwrap_or_else(|e| e.into_inner()) = Some(shipper);
}
pub(crate) fn backup_inbox(&self) -> Option<Arc<crate::backup::Inbox>> {
self.backup
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
.map(|b| b.inbox())
}
}
const DIRECT_SPIN_US: u64 = 50;
fn log_thread(log: Weak<SharedWal>) {
let Some((wake, woken, stop)) = log
.upgrade()
.map(|s| (s.wake.clone(), s.woken.clone(), s.stop.clone()))
else {
return;
};
loop {
{
let Some(shared) = log.upgrade() else { return };
shared.spin(|| shared.kicked.load(Ordering::Acquire));
shared.kicked.store(false, Ordering::Release);
}
{
let mut w = woken.lock().unwrap_or_else(|e| e.into_inner());
if !w.kicked && !stop.load(Ordering::Acquire) {
w.waiting = true;
w = wake
.wait_timeout(w, std::time::Duration::from_millis(100))
.unwrap_or_else(|e| e.into_inner())
.0;
w.waiting = false;
}
if stop.load(Ordering::Acquire) {
return;
}
if !w.kicked {
continue;
}
w.kicked = false;
}
let Some(shared) = log.upgrade() else { return };
let mut wal = shared.wal.lock().unwrap_or_else(|e| e.into_inner());
shared.start_writes(&mut wal);
}
}
fn blob_thread(log: Weak<SharedWal>) {
let Some((ready, kicked, stop)) = log
.upgrade()
.map(|s| (s.blob_ready.clone(), s.blob_kicked.clone(), s.stop.clone()))
else {
return;
};
loop {
{
let mut k = kicked.lock().unwrap_or_else(|e| e.into_inner());
if !*k && !stop.load(Ordering::Acquire) {
k = ready
.wait_timeout(k, std::time::Duration::from_millis(100))
.unwrap_or_else(|e| e.into_inner())
.0;
}
if stop.load(Ordering::Acquire) {
return;
}
if !*k {
continue;
}
*k = false;
}
let Some(shared) = log.upgrade() else { return };
loop {
let jobs = shared
.wal
.lock()
.unwrap_or_else(|e| e.into_inner())
.take_blob_jobs();
let Some(last) = jobs.last().map(|j| j.id) else {
break;
};
let mut files: Vec<u64> = Vec::new();
let mut written = Ok(());
for j in &jobs {
written = j.perform(&*shared.blob_dev);
if written.is_err() {
break;
}
if !files.contains(&j.file) {
files.push(j.file);
}
}
if written.is_ok() {
written = files.iter().try_for_each(|f| shared.blob_dev.sync(*f));
}
let mut wal = shared.wal.lock().unwrap_or_else(|e| e.into_inner());
match written.and_then(|()| wal.blobs_written(last)) {
Ok(()) => shared.start_writes(&mut wal),
Err(e) => {
shared.fail(e);
return;
}
}
}
}
}
fn io_thread(log: Weak<SharedWal>) {
let Some((jobs, job_ready, stop)) = log
.upgrade()
.map(|s| (s.jobs.clone(), s.job_ready.clone(), s.stop.clone()))
else {
return;
};
loop {
{
let Some(shared) = log.upgrade() else { return };
if shared.direct.load(Ordering::Relaxed) {
let budget = match shared.spin_cap_us {
0 => std::time::Duration::ZERO,
_ => shared
.spin_budget()
.max(std::time::Duration::from_micros(DIRECT_SPIN_US)),
};
crate::spin::spin_until(budget, || shared.queued.load(Ordering::Acquire) > 0);
} else {
shared.spin(|| shared.queued.load(Ordering::Acquire) > 0);
}
}
let job = {
let mut j = jobs.lock().unwrap_or_else(|e| e.into_inner());
if j.queue.is_empty() && !stop.load(Ordering::Acquire) {
j.idle += 1;
j = job_ready
.wait_timeout(j, std::time::Duration::from_millis(100))
.unwrap_or_else(|e| e.into_inner())
.0;
j.idle -= 1;
}
if stop.load(Ordering::Acquire) {
return;
}
j.queue.pop_front()
};
let Some((mut w, started)) = job else {
continue;
};
let Some(shared) = log.upgrade() else { return };
shared.queued.fetch_sub(1, Ordering::AcqRel);
let synced = Wal::perform(&shared.dev, &mut w);
shared
.last_flush_us
.store(started.elapsed().as_micros() as u64, Ordering::Relaxed);
let mut wakes = Vec::new();
{
let mut wal = shared.wal.lock().unwrap_or_else(|e| e.into_inner());
match synced {
Ok(()) => {
let shards = wal.complete(w);
shared.publish(&wal, &shards, None, &mut wakes);
shared.next_write(&mut wal, None, &mut wakes);
}
Err(e) => shared.fail(e),
}
}
for w in wakes {
w.run();
}
}
}
impl Drop for SharedWal {
fn drop(&mut self) {
let backup = self
.backup
.get_mut()
.unwrap_or_else(|e| e.into_inner())
.take();
let backed_up = backup.is_some();
if let Some(b) = backup {
b.finish(self);
}
if !backed_up
&& !self.has_failed.load(Ordering::Acquire)
&& self
.staging
.get_mut()
.unwrap_or_else(|e| e.into_inner())
.is_empty()
{
let wal = self.wal.get_mut().unwrap_or_else(|e| e.into_inner());
if let Err(e) = wal.shorten(unix_ms()) {
crate::log::warn(format!("the log was not shortened at close: {e}"));
}
}
self.stop.store(true, Ordering::Release);
if self.owner {
let one: u64 = 1;
unsafe { libc::write(self.owner_fd, (&raw const one).cast(), 8) };
}
self.wake.notify_all();
self.job_ready.notify_all();
self.blob_ready.notify_all();
let _ = self.threads.lock().map(|mut t| t.clear());
}
}
pub struct WalEnv {
inner: LocalEnv,
wal: Arc<SharedWal>,
shard: u32,
shard_dir: PathBuf,
in_flight: Option<Lsn>,
reported: Lsn,
checkpoints: Vec<Lsn>,
retained: Vec<Lsn>,
}
impl WalEnv {
pub fn open(
shard_dir: &Path,
shard: u32,
opts: LocalOptions,
wal: Arc<SharedWal>,
) -> io::Result<Self> {
let mut inner = LocalEnv::open(shard_dir, opts)?;
inner.keep_twin_checkpoints();
if let Some(inbox) = wal.backup_inbox() {
inner.set_backup(shard, inbox);
}
let reported = wal.shards[shard as usize].durable.load(Ordering::Acquire);
Ok(Self {
reported,
inner,
wal,
shard,
shard_dir: shard_dir.to_owned(),
in_flight: None,
checkpoints: Vec::new(),
retained: Vec::new(),
})
}
fn report_retained(&mut self) -> io::Result<()> {
if let Some(min) = self.retained.iter().copied().min() {
self.wal.set_retain(self.shard, min)?;
}
Ok(())
}
fn retained_on_disk(&self) -> io::Result<Vec<Lsn>> {
let mut cps: Vec<PathBuf> = fs::read_dir(&self.shard_dir)?
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.extension().is_some_and(|x| x == "fck"))
.collect();
cps.sort();
let mut out = Vec::new();
for path in cps.iter().rev() {
if out.len() == 2 {
break;
}
if let Ok(cp) = Checkpoint::decode(&fs::read(path)?) {
out.push(cp.lsn);
}
}
out.reverse();
Ok(out)
}
fn cover_twice(&mut self) {
let Some(&newest) = self.retained.last() else {
return;
};
let covered = self.retained.len() == 2 && self.retained[0] == newest;
let durable = self.wal.shards[self.shard as usize]
.durable
.load(Ordering::Acquire);
if covered || newest == 0 || newest < durable || !self.checkpoints.is_empty() {
return;
}
let twinned = self.inner.twin_checkpoint(newest).and_then(|made| {
if made {
self.retained = vec![newest, newest];
self.report_retained()?;
}
Ok(())
});
if let Err(e) = twinned {
crate::log::warn(format!(
"shard {}: no second copy of its last checkpoint at close: {e}",
self.shard
));
}
}
}
impl Env for WalEnv {
fn now_ms(&self) -> u64 {
self.inner.now_ms()
}
fn monotonic_us(&self) -> u64 {
self.inner.monotonic_us()
}
fn random(&mut self) -> u64 {
self.inner.random()
}
fn set_notify(&mut self, notify: Notify) {
*self.wal.shards[self.shard as usize]
.notify
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(notify.clone());
self.inner.set_notify(notify);
}
fn submit(&mut self, batch: Batch) {
self.in_flight = Some(batch.last_lsn);
self.wal.submit(self.shard, batch);
}
fn max_in_flight(&self) -> usize {
self.wal.in_flight_limit
}
fn log_stats(&self) -> crate::env::LogStats {
if self.shard != 0 {
return crate::env::LogStats::default();
}
let [w, b, r] = &self.wal.stats;
crate::env::LogStats {
writes: w.load(Ordering::Relaxed),
device_bytes: b.load(Ordering::Relaxed),
rolls: r.load(Ordering::Relaxed),
owner: self.wal.owner,
}
}
fn supports_inline(&self) -> bool {
self.inner.options().emulated_flush.is_zero()
}
fn flush_inline(&mut self, batch: Batch) -> io::Result<Lsn> {
self.wal.write_now(self.shard, batch)
}
fn last_flush_us(&self) -> u64 {
self.wal.last_flush_us.load(Ordering::Relaxed)
}
fn poll_flushed(&mut self) -> Option<io::Result<Lsn>> {
let want = self.in_flight?;
if let Some(m) = self.wal.fail_message() {
self.in_flight = None;
return Some(Err(io_other(m)));
}
let durable = self.wal.shards[self.shard as usize]
.durable
.load(Ordering::Acquire);
if durable >= want {
self.in_flight = None;
self.reported = durable;
return Some(Ok(durable));
}
if durable > self.reported {
self.reported = durable;
return Some(Ok(durable));
}
None
}
fn store_append(&mut self, frame: &[u8]) -> io::Result<u64> {
self.inner.store_append(frame)
}
fn store_append_history(&mut self, frame: &[u8], run: RunId, lsn: Lsn) -> io::Result<u64> {
self.inner.store_append_history(frame, run, lsn)
}
fn store_read(&mut self, offset: u64, len: u32) -> io::Result<Vec<u8>> {
self.inner.store_read(offset, len)
}
fn store_len(&self) -> u64 {
self.inner.store_len()
}
fn store_generation(&self) -> u64 {
self.inner.store_generation()
}
fn compact_store(&mut self, live: &[(RunId, BlobRef)]) -> io::Result<bool> {
self.inner.compact_store(live)
}
fn visibility_candidates(
&mut self,
filter: &crate::visibility::ListFilter,
limit: usize,
) -> Option<crate::visibility::Candidates> {
self.inner.visibility_candidates(filter, limit)
}
fn checkpoint(&mut self, checkpoint: Checkpoint, _delete_through_segment: Option<u64>) {
self.checkpoints.push(checkpoint.lsn);
self.inner.checkpoint(checkpoint, None);
}
fn poll_checkpoint(&mut self) -> Option<io::Result<Lsn>> {
let r = self.inner.poll_checkpoint()?;
if let Ok(lsn) = &r
&& let Some(i) = self.checkpoints.iter().position(|l| l == lsn)
{
self.checkpoints.remove(i);
self.retained.push(*lsn);
if self.retained.len() > 2 {
self.retained.remove(0);
}
if let Err(e) = self.report_retained() {
return Some(Err(e));
}
}
Some(r)
}
fn load_checkpoint(&mut self) -> io::Result<Option<Checkpoint>> {
let cp = self.inner.load_checkpoint()?;
self.retained = self.retained_on_disk()?;
if cp.is_none() {
self.retained = vec![0];
}
self.report_retained()?;
Ok(cp)
}
fn load_store(&mut self, generation: u64, len: u64) -> io::Result<Vec<u8>> {
self.inner.load_store(generation, len)
}
fn recover_store(
&mut self,
generation: u64,
len: u64,
visit: &mut dyn FnMut(u64, u32, fusee_core::store::StoreEntry),
) -> io::Result<()> {
self.inner.recover_store(generation, len, visit)
}
fn segments(&mut self) -> io::Result<Vec<u64>> {
Ok(Vec::new())
}
fn read_segment(&mut self, _segment: u64) -> io::Result<Vec<u8>> {
Err(io_other("a shared log has no per-shard segments"))
}
fn sync_recovered(&mut self) -> io::Result<()> {
Ok(())
}
fn discard_segment(&mut self, _segment: u64) -> io::Result<()> {
Ok(())
}
fn open_log(&mut self, _segment: u64) -> io::Result<()> {
Ok(())
}
fn exclusive_log(&self) -> bool {
true
}
fn shared_log(&self) -> bool {
true
}
fn recover_log(&mut self, after: Lsn) -> io::Result<(Vec<(Lsn, RecordBuf)>, Lsn)> {
self.wal.take_recovered(self.shard, after)
}
fn log_position(&self) -> u64 {
self.wal.position.load(Ordering::Acquire)
}
fn log_segment(&self) -> u64 {
self.wal.segment.load(Ordering::Acquire)
}
fn checkpoint_wanted(&self) -> bool {
self.wal.pins(self.shard)
}
fn checkpoint_on_roll(&self) -> bool {
self.wal
.wal
.lock()
.unwrap_or_else(|e| e.into_inner())
.holds_retiring(self.shard)
}
fn checkpoint_wanted_on_close(&self) -> bool {
self.wal.holds_old(self.shard)
}
fn report_due(&mut self, due: crate::runtime::Due, closed: bool) {
self.inner.report_due(due, closed);
if closed {
self.cover_twice();
}
}
}
#[cfg(test)]
mod tests;
#[cfg(all(target_os = "linux", feature = "io-uring"))]
mod owner;