use super::*;
use std::collections::BTreeSet;
impl WALManager {
pub fn create_checkpoint(&self) -> Result<u64> {
let mut transition = self.transition.lock().unwrap();
match transition.lifecycle {
WalLifecycle::Running | WalLifecycle::Failed => {}
WalLifecycle::Closing | WalLifecycle::Closed => return Err(Error::WalNotRunning),
}
self.flush_under_transition()?;
if let Err(error) = self.sync_locked() {
return Err(self.mark_transition_uncertain(
&mut transition,
format!("checkpoint WAL fsync failed: {}", error),
));
}
if transition.lifecycle == WalLifecycle::Failed {
transition.lifecycle = WalLifecycle::Running;
self.running.store(true, Ordering::Release);
}
let checkpoint_lsn = self.current_lsn.load(Ordering::Acquire);
#[cfg(any(test, feature = "test-failpoints"))]
crate::test_failpoints::interleave(
crate::test_failpoints::InterleavePoint::CheckpointBeforePublish,
0,
);
#[cfg(any(test, feature = "test-failpoints"))]
if crate::test_failpoints::CHECKPOINT_WRITE_FAIL.load(std::sync::atomic::Ordering::Acquire)
{
return Err(Error::internal("failpoint: checkpoint write"));
}
self.last_checkpoint
.store(checkpoint_lsn, Ordering::Release);
Ok(checkpoint_lsn)
}
pub fn close(&self) -> Result<()> {
let mut transition = self.transition.lock().unwrap();
match transition.lifecycle {
WalLifecycle::Closed => return Ok(()),
WalLifecycle::Closing => {
return Err(Error::internal("WAL close is already in progress"));
}
WalLifecycle::Running | WalLifecycle::Failed => {
transition.lifecycle = WalLifecycle::Closing;
self.running.store(false, Ordering::Release);
}
}
drop(transition);
#[cfg(test)]
let close_test_hook = {
self.close_test_hook
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
};
#[cfg(test)]
if let Some(hook) = close_test_hook {
hook(&self.path);
}
let mut transition = self.transition.lock().unwrap();
if let Err(error) = self.flush_under_transition() {
transition.lifecycle = WalLifecycle::Failed;
return Err(Error::WalDurabilityUncertain {
detail: format!("WAL close could not drain accepted bytes: {}", error),
});
}
if let Err(error) = self.sync_locked() {
return Err(self.mark_transition_uncertain(
&mut transition,
format!("WAL close fsync failed: {}", error),
));
}
let mut wal_file = self.wal_file.lock().unwrap();
*wal_file = None;
transition.lifecycle = WalLifecycle::Closed;
Ok(())
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn max_wal_size(&self) -> u64 {
self.max_wal_size
}
pub fn last_checkpoint_lsn(&self) -> u64 {
self.last_checkpoint.load(Ordering::Acquire)
}
pub fn current_wal_file(&self) -> String {
self.current_wal_file.lock().unwrap().clone()
}
pub(crate) fn prepare_checkpoint_retention(
&self,
up_to_lsn: u64,
) -> Result<crate::v6::WalGeneration> {
let mut transition = self.transition.lock().unwrap();
Self::require_running_transition(&transition)?;
if up_to_lsn == 0 {
return Err(Error::internal(
"WAL retention requires a non-zero checkpoint LSN",
));
}
let current_lsn = self.current_lsn.load(Ordering::Acquire);
let checkpoint_lsn = self.last_checkpoint.load(Ordering::Acquire);
if up_to_lsn > current_lsn || up_to_lsn > checkpoint_lsn {
return Err(Error::internal(format!(
"WAL retention boundary {} is not owned by durable checkpoint {} at current LSN {}",
up_to_lsn, checkpoint_lsn, current_lsn
)));
}
self.flush_under_transition()?;
if let Err(error) = self.sync_locked() {
return Err(self.mark_transition_uncertain(
&mut transition,
format!("WAL retention seed preflight fsync failed: {}", error),
));
}
if self.current_file_position.load(Ordering::Acquire) > 0 && current_lsn <= up_to_lsn {
self.rotate_wal_under_transition(&mut transition)?;
}
crate::v6::WalGeneration::new(self.wal_sequence.load(Ordering::Acquire))
.map_err(|error| Error::internal(error.to_string()))
}
pub(crate) fn freeze_snapshot_generations(
&self,
floor: crate::v6::WalReplayFloor,
) -> Result<Vec<SnapshotWalGeneration>> {
let mut transition = self.transition.lock().unwrap();
Self::require_running_transition(&transition)?;
let replay_floor = *self
.replay_floor
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if floor != replay_floor {
return Err(Error::internal(format!(
"snapshot WAL floor {}:{} differs from the active WAL owner {}:{}",
floor.generation().get(),
floor.lsn(),
replay_floor.generation().get(),
replay_floor.lsn()
)));
}
self.flush_under_transition()?;
if let Err(error) = self.sync_locked() {
return Err(self.mark_transition_uncertain(
&mut transition,
format!("snapshot WAL boundary fsync failed: {error}"),
));
}
let last_snapshot_generation = self.wal_sequence.load(Ordering::Acquire);
self.rotate_wal_under_transition(&mut transition)?;
let closed = self
.validated_closed_generations
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let selected = closed
.iter()
.filter(|generation| {
generation.sequence >= floor.generation().get()
&& generation.sequence <= last_snapshot_generation
})
.collect::<Vec<_>>();
let expected_count = last_snapshot_generation
.checked_sub(floor.generation().get())
.and_then(|distance| distance.checked_add(1))
.and_then(|count| usize::try_from(count).ok())
.ok_or_else(|| Error::internal("snapshot WAL generation range exceeds usize"))?;
if selected.len() != expected_count
|| selected.iter().enumerate().any(|(offset, generation)| {
generation.sequence
!= floor
.generation()
.get()
.checked_add(offset as u64)
.unwrap_or(0)
})
{
return Err(Error::internal(
"snapshot WAL suffix is not contiguous from the CONTROL replay floor",
));
}
let mut sources = Vec::with_capacity(selected.len());
for generation in selected {
let identity = WalFileIdentity::read(&generation.path)?;
if identity != generation.identity {
return Err(Error::internal(format!(
"snapshot WAL generation {} changed after validation",
generation.path.display()
)));
}
sources.push(SnapshotWalGeneration {
generation: crate::v6::WalGeneration::new(generation.sequence)
.map_err(|error| Error::internal(error.to_string()))?,
path: generation.path.clone(),
byte_length: identity.len,
});
}
Ok(sources)
}
pub(crate) fn checkpoint_retirement_candidates(
&self,
retained_floor: crate::v6::WalGeneration,
) -> Result<Vec<crate::v6::WalGeneration>> {
let transition = self.transition.lock().unwrap();
Self::require_running_transition(&transition)?;
let mut generations = BTreeSet::new();
collect_retirement_candidates(&self.path, retained_floor.get(), &mut generations)?;
let retired = self.path.join("retired");
if retired.exists() {
collect_retirement_candidates(&retired, retained_floor.get(), &mut generations)?;
}
generations
.into_iter()
.map(|generation| {
crate::v6::WalGeneration::new(generation)
.map_err(|error| Error::internal(error.to_string()))
})
.collect()
}
pub(crate) fn confirm_checkpoint_publication(
&self,
floor: crate::v6::WalReplayFloor,
retired: &[crate::v6::WalGeneration],
) {
*self
.replay_floor
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()) = floor;
if retired.is_empty() {
return;
}
let retired = retired
.iter()
.map(|generation| generation.get())
.collect::<BTreeSet<_>>();
self.validated_closed_generations
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.retain(|generation| !retired.contains(&generation.sequence));
}
}
fn collect_retirement_candidates(
directory: &Path,
retained_floor: u64,
generations: &mut BTreeSet<u64>,
) -> Result<()> {
let metadata = fs::symlink_metadata(directory).map_err(|error| {
Error::internal(format!(
"failed to inspect WAL retirement directory {}: {}",
directory.display(),
error
))
})?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(Error::internal(format!(
"WAL retirement path {} is not a regular directory",
directory.display()
)));
}
for entry in fs::read_dir(directory).map_err(|error| {
Error::internal(format!(
"failed to enumerate WAL retirement directory {}: {}",
directory.display(),
error
))
})? {
let entry = entry.map_err(|error| {
Error::internal(format!("failed to read WAL retirement entry: {}", error))
})?;
let name = entry.file_name().to_string_lossy().to_string();
if name == "retired" && entry.path() == directory.join("retired") {
continue;
}
let generation = WALManager::parse_canonical_generation(&name).ok_or_else(|| {
Error::internal(format!(
"non-canonical file '{}' exists in WAL retirement directory {}",
name,
directory.display()
))
})?;
let entry_type = entry.file_type().map_err(|error| {
Error::internal(format!(
"failed to inspect WAL retirement entry '{}': {}",
name, error
))
})?;
if entry_type.is_symlink() || !entry_type.is_file() {
return Err(Error::internal(format!(
"WAL retirement entry '{}' is not a regular file",
name
)));
}
if generation < retained_floor {
generations.insert(generation);
}
}
Ok(())
}
impl Drop for WALManager {
fn drop(&mut self) {
let _ = self.close();
}
}