use std::io::{Read, Write};
use super::super::dir::StoreFile;
use super::files::DirPath;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use super::super::codec::{Reader, crc, put_u32};
use super::super::error::PersistError;
use super::super::journal::JournalFiles;
use super::super::wal::{JournalReader, JournalRecord, WAL_HEADER_BYTES, WalHeader, encode_mark};
use super::files::{GraphFile, MARKS_FILE, WAL_DIR, sync_dir, wal_name, write_graph_file};
use crate::graph::hooks::Mark;
#[derive(Debug, Default)]
pub(crate) struct WalShared {
pub(crate) closed: AtomicBool,
pub(crate) frozen: Mutex<Option<(String, usize)>>,
pub(crate) rotate: AtomicBool,
pub(crate) since_checkpoint: AtomicU64,
pub(crate) checkpoint_after: AtomicU64,
pub(crate) wake: Mutex<bool>,
pub(crate) wake_signal: Condvar,
}
impl WalShared {
pub(crate) fn frozen(&self) -> Option<(String, usize)> {
self.frozen.lock().ok().and_then(|f| f.clone())
}
pub(crate) fn wake(&self) {
if let Ok(mut due) = self.wake.lock() {
*due = true;
self.wake_signal.notify_all();
}
}
}
pub(crate) struct WalWriter {
pub(crate) file: Box<dyn StoreFile>,
}
impl Write for WalWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.file.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.file.flush()
}
}
pub(crate) struct StoreWalFiles {
pub(crate) dir: DirPath,
pub(crate) graph_id: u128,
pub(crate) segment_bytes: u64,
pub(crate) current: u64,
pub(crate) shared: Arc<WalShared>,
pub(crate) broken: bool,
pub(crate) graph_file: Arc<Mutex<GraphFile>>,
pub(crate) faults: Option<Arc<Faults>>,
}
#[derive(Debug, Default)]
pub(crate) struct Faults {
pub(crate) fail_sync: AtomicBool,
pub(crate) fail_truncate: AtomicBool,
}
impl Faults {
pub(crate) fn sync(&self) -> std::io::Result<()> {
if self.fail_sync.load(Ordering::Acquire) {
return Err(std::io::Error::other("injected fsync failure"));
}
Ok(())
}
}
pub(crate) fn create_segment(
dir: &DirPath,
graph_id: u128,
first: u64,
) -> std::io::Result<Box<dyn StoreFile>> {
let path = dir.join(WAL_DIR).join(wal_name(first));
let header = WalHeader {
graph_id,
first_commit_seq: first,
created_at: crate::graph::undo::now_micros(),
};
let created = path.create(true).and_then(|mut file| {
file.write_all(&header.encode())?;
file.sync()?;
Ok(file)
});
match created {
Ok(file) => {
sync_dir(&dir.join(WAL_DIR)).map_err(|err| std::io::Error::other(err.to_string()))?;
Ok(file)
}
Err(err) => {
if err.kind() != std::io::ErrorKind::AlreadyExists {
let _ = path.remove_file();
}
Err(err)
}
}
}
pub(crate) fn record_wal_head(dir: &DirPath, graph_file: &Mutex<GraphFile>, first: u64) {
let Ok(mut file) = graph_file.lock() else {
return;
};
if file.wal_head == Some(first) {
return;
}
file.wal_head = Some(first);
if let Err(err) = write_graph_file(dir, &file) {
tracing::warn!("recording the new WAL segment {first} in GRAPH failed: {err}");
}
}
pub(crate) fn open_segment_at(path: &DirPath, offset: u64) -> std::io::Result<Box<dyn StoreFile>> {
path.open_write(offset)
}
impl JournalFiles<WalWriter> for StoreWalFiles {
fn before_record(
&mut self,
writer: &mut WalWriter,
offset: u64,
next_seq: u64,
) -> std::io::Result<Option<(u64, u64)>> {
if self.shared.closed.load(Ordering::Acquire) {
return Err(std::io::Error::other("the store is closed"));
}
if let Some((found_by, problems)) = self.shared.frozen() {
return Err(std::io::Error::other(format!(
"the store is read-only: {found_by} found damage on disk ({problems} problem(s)) \
and its damage policy is \"maintenance\""
)));
}
if self.broken {
return Err(std::io::Error::other(
"an earlier WAL segment could not be started; reopen the store",
));
}
let wanted = offset > self.segment_bytes || self.shared.rotate.load(Ordering::Acquire);
if !wanted || self.current == next_seq {
return Ok(None);
}
writer.file.sync()?;
let file = match create_segment(&self.dir, self.graph_id, next_seq) {
Ok(file) => file,
Err(err) => {
let path = self.dir.join(WAL_DIR).join(wal_name(next_seq));
if path.exists() && err.kind() != std::io::ErrorKind::AlreadyExists {
self.broken = path.remove_file().is_err();
}
return Err(err);
}
};
writer.file = file;
self.current = next_seq;
self.shared.rotate.store(false, Ordering::Release);
record_wal_head(&self.dir, &self.graph_file, next_seq);
Ok(Some((next_seq, WAL_HEADER_BYTES as u64)))
}
fn record_written(&mut self, bytes: u64) {
let total = self
.shared
.since_checkpoint
.fetch_add(bytes, Ordering::AcqRel)
+ bytes;
let limit = self.shared.checkpoint_after.load(Ordering::Acquire);
if limit > 0 && total >= limit {
self.shared.wake();
}
}
fn discard(
&mut self,
writer: &mut WalWriter,
segment: u64,
offset: u64,
) -> std::io::Result<()> {
let injected = self
.faults
.as_ref()
.is_some_and(|f| f.fail_truncate.load(Ordering::Acquire));
let cut = if injected {
Err(std::io::Error::other("injected truncate failure"))
} else {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| writer.file.set_len(offset)))
.unwrap_or_else(|payload| {
Err(std::io::Error::other(format!(
"the cut panicked: {}",
super::super::journal::panic_text(payload.as_ref())
)))
})
};
let Err(err) = cut else {
return Ok(());
};
let mut file = self
.graph_file
.lock()
.map_err(|_| std::io::Error::other("a store lock is poisoned"))?;
file.wal_cut = Some((segment, offset));
if let Err(write) = write_graph_file(&self.dir, &file) {
tracing::error!(
"the refused WAL record at segment {segment}, offset {offset} could neither be \
cut off ({err}) nor recorded ({write}); do not reopen the store before checking it"
);
}
Err(err)
}
fn mark_written(&mut self, mark: &Mark) {
if append_mark(&self.dir, mark).is_err() {
let _ = self.dir.join(MARKS_FILE).remove_file();
}
}
}
fn mark_frame(mark: &Mark) -> Vec<u8> {
let payload = encode_mark(mark);
let mut out = Vec::with_capacity(payload.len() + 8);
put_u32(&mut out, payload.len() as u32);
put_u32(&mut out, crc(&payload));
out.extend_from_slice(&payload);
out
}
pub(crate) fn append_mark(dir: &DirPath, mark: &Mark) -> std::io::Result<()> {
dir.join(MARKS_FILE).append(&mark_frame(mark))
}
pub(crate) fn write_marks(dir: &DirPath, marks: &[Mark]) -> Result<(), PersistError> {
super::files::write_atomic(dir, MARKS_FILE, &marks_bytes(marks))
}
pub(crate) fn marks_bytes(marks: &[Mark]) -> Vec<u8> {
let mut bytes = Vec::new();
for mark in marks {
bytes.extend_from_slice(&mark_frame(mark));
}
bytes
}
pub(crate) fn read_marks(dir: &DirPath) -> Result<Option<Vec<Mark>>, PersistError> {
let path = dir.join(MARKS_FILE);
let bytes = match super::files::read_bounded(&path, 256 << 20) {
Ok(bytes) => bytes,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(_) => return Ok(None),
};
Ok(decode_marks(&bytes))
}
pub(crate) fn reachable_marks(
dir: &DirPath,
mut marks: Vec<Mark>,
) -> Result<Vec<Mark>, PersistError> {
if let Some((oldest, _)) = super::files::list_snapshots(dir)?.first() {
marks.retain(|mark| mark.commit_seq() >= *oldest);
}
Ok(marks)
}
pub(crate) fn decode_marks(bytes: &[u8]) -> Option<Vec<Mark>> {
let mut marks = Vec::new();
let mut r = Reader::new(bytes);
while !r.is_empty() {
let length = r.u32().ok()? as usize;
let stored = r.u32().ok()?;
let payload = r.bytes(length).ok()?;
if crc(payload) != stored {
return None;
}
let mut p = Reader::new(payload);
let commit_seq = p.u64().ok()?;
let time = p.i64().ok()?;
let name = p.str().ok()?.to_string();
if !p.is_empty() {
return None;
}
marks.push(Mark::new(name, commit_seq, time));
}
Some(marks)
}
pub(crate) fn open_segment_reader(
path: &DirPath,
limit: Option<u64>,
) -> Result<Box<dyn Read + Send>, PersistError> {
let reader = path
.open_read()
.map_err(|err| PersistError::io("opening a WAL segment", err))?;
Ok(match limit {
Some(limit) => Box::new(reader.take(limit)),
None => Box::new(reader),
})
}
pub(crate) fn scan_marks(segments: &[(u64, DirPath)], max_depth: usize) -> Vec<Mark> {
let mut marks = Vec::new();
for (index, (_, path)) in segments.iter().enumerate() {
let Ok(source) = open_segment_reader(path, None) else {
continue;
};
let Ok(mut reader) = JournalReader::open(source, index, max_depth) else {
continue;
};
while let Ok(Some((_, _, record))) = reader.next_record() {
if let JournalRecord::Mark(mark) = record {
marks.push(mark);
}
}
}
marks
}
pub(crate) fn wal_set_problems(
segments: &[(u64, DirPath)],
file: &GraphFile,
) -> Vec<(String, String)> {
let mut problems = Vec::new();
let head = file.wal_head;
for (first, path) in segments {
let name = format!("{WAL_DIR}/{}", wal_name(*first));
let len = path.len().unwrap_or(0);
if len < WAL_HEADER_BYTES as u64 {
if head.is_some_and(|head| *first <= head) {
problems.push((
name,
format!(
"the segment is shorter than its header ({len} bytes), but it was \
started and made durable before (GRAPH records segment {} as the \
newest): its records are lost",
head.unwrap_or(0)
),
));
}
continue;
}
let Ok(bytes) = path.read_range(0, WAL_HEADER_BYTES) else {
continue;
};
if let Ok(header) = WalHeader::decode(&bytes)
&& header.first_commit_seq != *first
{
problems.push((
name,
format!(
"its header names first commit {}, its file name {first}: the file holds \
another segment's bytes, its own records are lost",
header.first_commit_seq
),
));
}
}
if let Some(head) = head
&& !segments.iter().any(|(first, _)| *first == head)
{
problems.push((
format!("{WAL_DIR}/{}", wal_name(head)),
format!(
"the newest WAL segment GRAPH records (first commit {head}) is missing: the \
commits from {head} on are lost"
),
));
}
problems
}