use std::io::{Read as _, Seek as _, SeekFrom};
use std::path::{Path, PathBuf};
use std::time::Duration;
pub const DEFAULT_ROTATE_AT_BYTES: u64 = 4 * 1024 * 1024;
pub const DEFAULT_ROTATED_KEEP: usize = 2;
const ROTATION_LOCK_TIMEOUT: Duration = Duration::from_millis(50);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RotationPolicy {
pub max_bytes: u64,
pub keep: usize,
}
impl Default for RotationPolicy {
fn default() -> Self {
Self {
max_bytes: DEFAULT_ROTATE_AT_BYTES,
keep: DEFAULT_ROTATED_KEEP,
}
}
}
impl RotationPolicy {
fn keep(self) -> usize {
self.keep.max(1)
}
pub(crate) fn read_limit(self) -> u64 {
self.max_bytes.max(1).saturating_mul(2)
}
}
pub(crate) fn rotated_path(path: &Path, n: usize) -> PathBuf {
let mut name = path.as_os_str().to_os_string();
name.push(format!(".{n}"));
PathBuf::from(name)
}
pub(crate) fn files_newest_first(path: &Path, policy: RotationPolicy) -> Vec<PathBuf> {
let mut files = vec![path.to_path_buf()];
files.extend((1..=policy.keep()).map(|n| rotated_path(path, n)));
files
}
pub(crate) fn rotate_if_due(path: &Path, policy: RotationPolicy) -> std::io::Result<()> {
if !is_due(path, policy) {
return Ok(());
}
crate::file_lock::with_exclusive_lock_timeout(path, ROTATION_LOCK_TIMEOUT, || {
if !is_due(path, policy) {
return Ok(());
}
for n in (1..policy.keep()).rev() {
let from = rotated_path(path, n);
if from.exists() {
std::fs::rename(&from, rotated_path(path, n + 1))?;
}
}
move_live_into_first_slot(path, policy)
})?
}
fn is_due(path: &Path, policy: RotationPolicy) -> bool {
std::fs::metadata(path).is_ok_and(|m| m.is_file() && m.len() >= policy.max_bytes)
}
fn with_suffix(path: &Path, suffix: &str) -> PathBuf {
let mut name = path.as_os_str().to_os_string();
name.push(suffix);
PathBuf::from(name)
}
fn move_live_into_first_slot(path: &Path, policy: RotationPolicy) -> std::io::Result<()> {
let first = rotated_path(path, 1);
let len = std::fs::metadata(path)?.len();
let src = with_suffix(&first, ".src");
if len <= policy.read_limit() || src.is_file() {
return std::fs::rename(path, first);
}
compact_legacy_into_first_slot(path, &first, &src, policy)
}
fn compact_legacy_into_first_slot(
path: &Path,
first: &Path,
src: &Path,
policy: RotationPolicy,
) -> std::io::Result<()> {
std::fs::rename(path, src)?;
let tmp = with_suffix(first, ".tmp");
let compacted = write_compacted(src, &tmp, policy.max_bytes).and_then(|()| {
super::test_hook::fire(super::test_hook::Point::CompactionCopied);
std::fs::rename(&tmp, first)
});
if let Err(e) = compacted {
eprintln!(
"[bug-capture] compacting {} failed ({e}); keeping it whole",
src.display()
);
let _ = std::fs::remove_file(&tmp);
return std::fs::rename(src, first);
}
if let Err(e) = std::fs::remove_file(src) {
eprintln!("[bug-capture] removing {}: {e}", src.display());
}
Ok(())
}
fn write_compacted(src: &Path, tmp: &Path, limit: u64) -> std::io::Result<()> {
let mut source = std::fs::File::open(src)?;
let (tail, read_to) = read_tail_from(&mut source, limit)?;
super::test_hook::fire(super::test_hook::Point::CompactionTailRead);
let mut out = std::fs::File::create(tmp)?;
std::io::Write::write_all(&mut out, &tail)?;
source.seek(SeekFrom::Start(read_to))?;
std::io::copy(&mut source, &mut out)?;
out.sync_all()
}
pub(crate) fn read_tail_from(
file: &mut std::fs::File,
limit: u64,
) -> std::io::Result<(Vec<u8>, u64)> {
let len = file.metadata()?.len();
let start_at = len.saturating_sub(limit);
file.seek(SeekFrom::Start(start_at))?;
let mut buf = Vec::new();
let read = file.read_to_end(&mut buf)? as u64;
let end = start_at + read;
if start_at == 0 {
return Ok((buf, end));
}
let start = buf
.iter()
.position(|b| *b == b'\n')
.map_or(buf.len(), |i| i + 1);
Ok((buf.split_off(start), end))
}