use super::CheckpointEntry;
use crate::store::cold_start::{FileLoad, ReservedKindFallbackStats};
use crate::store::index::RoutingSummary;
use crate::store::platform::fs::{write_file_atomically_with_fs, StoreFs};
use crate::store::StoreError;
use serde::{Deserialize, Serialize};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
pub(super) const CHECKPOINT_MAGIC: &[u8; 6] = b"FBATCK";
pub const CHECKPOINT_VERSION: u16 = 6;
pub(crate) const CHECKPOINT_FILENAME: &str = "index.ckpt";
#[derive(Serialize, Deserialize)]
pub(super) struct CheckpointDataV6 {
pub(super) global_sequence: u64,
pub(super) watermark_segment_id: u64,
pub(super) watermark_offset: u64,
pub(super) interner_strings: Vec<String>,
pub(super) routing: RoutingSummary,
#[serde(default)]
pub(super) reserved_kind_fallbacks: ReservedKindFallbackStats,
pub(super) entries: Vec<CheckpointEntry>,
}
pub(super) struct LoadedCheckpointFile {
pub(super) path: PathBuf,
pub(super) version: u16,
pub(super) body: Vec<u8>,
}
pub(super) struct DecodedCheckpointData {
pub(super) entries: Vec<CheckpointEntry>,
pub(super) interner_strings: Vec<String>,
pub(super) watermark_segment_id: u64,
pub(super) watermark_offset: u64,
pub(super) global_sequence: u64,
pub(super) routing: RoutingSummary,
pub(super) cumulative_reserved_kind_fallbacks: ReservedKindFallbackStats,
}
pub(super) fn read_checkpoint_file(
data_dir: &Path,
fs: &dyn crate::store::platform::fs::StoreFs,
) -> FileLoad<LoadedCheckpointFile> {
let path = data_dir.join(CHECKPOINT_FILENAME);
let raw = match fs.read(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return FileLoad::Missing,
Err(error) => {
tracing::warn!(
target: "batpak::checkpoint",
path = %path.display(),
error = %error,
"failed to read checkpoint file"
);
return FileLoad::Invalid {
reason: format!("read failed: {error}"),
};
}
};
let (version, stored_crc, body) = match crate::store::wire_header::parse(&raw, CHECKPOINT_MAGIC)
{
Ok(prefix) => (prefix.version, prefix.stored_crc, prefix.body.to_vec()),
Err(crate::store::wire_header::PrefixError::TooShort { len }) => {
tracing::warn!(
target: "batpak::checkpoint",
path = %path.display(),
len,
"checkpoint file too short to contain a valid header"
);
return FileLoad::Invalid {
reason: format!("checkpoint file too short: {len} bytes"),
};
}
Err(crate::store::wire_header::PrefixError::BadMagic) => {
tracing::warn!(
target: "batpak::checkpoint",
path = %path.display(),
"checkpoint file has wrong magic bytes — ignoring"
);
return FileLoad::Invalid {
reason: "wrong magic bytes".to_owned(),
};
}
};
if version > CHECKPOINT_VERSION {
tracing::warn!(
target: "batpak::checkpoint",
path = %path.display(),
version,
supported = CHECKPOINT_VERSION,
"checkpoint declares a future format version — refusing canonically"
);
return FileLoad::FutureVersion {
found: version,
supported: CHECKPOINT_VERSION,
};
}
let computed_crc = crc32fast::hash(&body);
if stored_crc != computed_crc {
tracing::warn!(
target: "batpak::checkpoint",
path = %path.display(),
stored = stored_crc,
computed = computed_crc,
"checkpoint CRC mismatch — file is corrupt, ignoring"
);
return FileLoad::Invalid {
reason: format!("crc mismatch: stored {stored_crc}, computed {computed_crc}"),
};
}
FileLoad::Loaded(LoadedCheckpointFile {
path,
version,
body,
})
}
pub(super) fn decode_checkpoint_data(
path: &Path,
version: u16,
body: &[u8],
) -> Option<DecodedCheckpointData> {
match version {
5 | 6 => {
let data: CheckpointDataV6 =
decode_body(path, body, "checkpoint deserialisation failed — ignoring")?;
Some(DecodedCheckpointData {
entries: data.entries,
interner_strings: data.interner_strings,
watermark_segment_id: data.watermark_segment_id,
watermark_offset: data.watermark_offset,
global_sequence: data.global_sequence,
routing: data.routing,
cumulative_reserved_kind_fallbacks: data.reserved_kind_fallbacks,
})
}
_ => {
tracing::warn!(
target: "batpak::checkpoint",
path = %path.display(),
version,
expected = CHECKPOINT_VERSION,
"unsupported checkpoint version — ignoring"
);
None
}
}
}
pub(super) fn decode_checkpoint_snapshot_v6(path: &Path, body: &[u8]) -> Option<CheckpointDataV6> {
decode_body(
path,
body,
"checkpoint snapshot deserialisation failed — ignoring",
)
}
pub(super) fn encode_checkpoint_body<T: Serialize + ?Sized>(
body: &T,
) -> Result<Vec<u8>, rmp_serde::encode::Error> {
crate::encoding::to_bytes(body)
}
pub(super) fn write_checkpoint_file(
data_dir: &Path,
body: &[u8],
fs: &dyn StoreFs,
) -> Result<(), StoreError> {
let crc: u32 = crc32fast::hash(body);
let final_path = data_dir.join(CHECKPOINT_FILENAME);
write_file_atomically_with_fs(
data_dir,
&final_path,
"checkpoint",
|file| {
let mut w = BufWriter::new(file);
w.write_all(&crate::store::wire_header::encode(
CHECKPOINT_MAGIC,
CHECKPOINT_VERSION,
crc,
))?;
w.write_all(body)?;
w.flush()?;
Ok(())
},
fs,
)?;
Ok(())
}
fn decode_body<T: for<'de> Deserialize<'de>>(
path: &Path,
body: &[u8],
message: &'static str,
) -> Option<T> {
match crate::encoding::from_bytes(body) {
Ok(data) => Some(data),
Err(error) => {
tracing::warn!(
target: "batpak::checkpoint",
path = %path.display(),
error = %error,
message
);
None
}
}
}