use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use kevy_resp::{Argv, ArgvView};
pub(crate) const LOG_BASE: &[u8] = b"\0KEVYLOGBASE";
pub(crate) const STANDS_ALONE: u64 = 0;
const SNAPSHOT_ID_TAG: &[u8; 8] = b"KEVYSNID";
const FOOTER: usize = SNAPSHOT_ID_TAG.len() + 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LogHead {
Missing,
Legacy,
StandsAlone,
After(u64),
}
pub(crate) fn base_frame(snapshot_id: u64) -> Argv {
let mut frame = Argv::with_capacity(2, LOG_BASE.len() + 8);
frame.push(LOG_BASE);
frame.push(&snapshot_id.to_le_bytes());
frame
}
pub(crate) fn write_image_base<W: Write>(w: &mut W, scratch: &mut Vec<u8>) -> io::Result<()> {
crate::record::write_record_multibulk(w, &base_frame(STANDS_ALONE), scratch)
}
pub(crate) fn base_of<A: ArgvView + ?Sized>(args: &A) -> Option<u64> {
if args.len() != 2 || args.get(0) != Some(LOG_BASE) {
return None;
}
args.get(1).and_then(|id| <[u8; 8]>::try_from(id).ok()).map(u64::from_le_bytes)
}
pub fn is_log_base<A: ArgvView + ?Sized>(args: &A) -> bool {
base_of(args).is_some()
}
pub(crate) fn read_log_head(path: &Path) -> io::Result<LogHead> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(LogHead::Missing),
Err(e) => return Err(e),
};
let mut head = Vec::with_capacity(128);
Read::take(&mut file, 128).read_to_end(&mut head)?;
let magic = crate::record::AOF2_MAGIC;
if !head.starts_with(magic) {
return Ok(LogHead::Legacy);
}
let crate::RecordStep::Ok { payload, .. } = crate::next_record(&head, magic.len()) else {
return Ok(LogHead::Legacy);
};
let mut args = Argv::default();
if !matches!(kevy_resp::parse_command_into(payload, &mut args), Ok(Some(_))) {
return Ok(LogHead::Legacy);
}
Ok(match base_of(&args) {
Some(STANDS_ALONE) => LogHead::StandsAlone,
Some(id) => LogHead::After(id),
None => LogHead::Legacy,
})
}
pub(crate) fn fresh_snapshot_id() -> u64 {
static SEQ: AtomicU64 = AtomicU64::new(0);
let place = &SEQ as *const AtomicU64 as u64;
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let mut z = kevy_store::now_unix_ms()
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.wrapping_add(seq.wrapping_mul(0xBF58_476D_1CE4_E5B9))
^ place.rotate_left(29);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
z.max(1)
}
pub(crate) fn write_snapshot_id<W: Write>(w: &mut W) -> io::Result<u64> {
let id = fresh_snapshot_id();
let mut footer = [0u8; FOOTER];
footer[..SNAPSHOT_ID_TAG.len()].copy_from_slice(SNAPSHOT_ID_TAG);
footer[SNAPSHOT_ID_TAG.len()..].copy_from_slice(&id.to_le_bytes());
w.write_all(&footer)?;
Ok(id)
}
pub(crate) fn snapshot_id(path: &Path) -> io::Result<Option<u64>> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e),
};
if file.metadata()?.len() < FOOTER as u64 {
return Ok(None);
}
file.seek(SeekFrom::End(-(FOOTER as i64)))?;
let mut footer = [0u8; FOOTER];
file.read_exact(&mut footer)?;
let (tag, id) = footer.split_at(SNAPSHOT_ID_TAG.len());
if tag != SNAPSHOT_ID_TAG {
return Ok(None);
}
Ok(Some(u64::from_le_bytes(id.try_into().expect("the footer's second half is 8 bytes"))))
}
pub(crate) fn prev_path(snapshot: &Path) -> PathBuf {
let mut s = snapshot.as_os_str().to_owned();
s.push(".prev");
s.into()
}
pub fn settle_snapshot(snapshot: &Path, log: Option<&Path>) -> io::Result<bool> {
let head = match log {
Some(path) => read_log_head(path)?,
None => LogHead::Missing,
};
let prev = prev_path(snapshot);
if prev.exists() {
let committed = match head {
LogHead::After(id) => snapshot_id(snapshot)? == Some(id),
LogHead::Missing => snapshot.exists(),
LogHead::Legacy | LogHead::StandsAlone => false,
};
if committed {
std::fs::remove_file(&prev)?;
} else {
std::fs::rename(&prev, snapshot)?;
}
}
match head {
LogHead::StandsAlone => Ok(false),
LogHead::After(id) => match snapshot_id(snapshot)? {
Some(found) if found == id => Ok(true),
found => Err(unpaired(snapshot, log, id, found)),
},
LogHead::Legacy | LogHead::Missing => Ok(snapshot.exists()),
}
}
fn unpaired(snapshot: &Path, log: Option<&Path>, id: u64, found: Option<u64>) -> io::Error {
let log = log.map_or_else(String::new, |p| p.display().to_string());
let found = match found {
Some(other) => format!("is snapshot {other:016x}"),
None if snapshot.exists() => String::from("carries no snapshot id"),
None => String::from("is missing"),
};
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{log} holds the writes after snapshot {id:016x}, but {} {found}; \
restore that snapshot, or move the log aside to start from the snapshot alone",
snapshot.display()
),
)
}