fix(agent): crash between checkpoint and WAL truncate no longer duplicates entries
flush() writes the new .h5 and only then truncates the WAL. A crash in that window left a .h5 that already contained the pending entries AND a WAL that still listed them, and open() replayed the WAL unconditionally — every pending entry came back twice. A checkpoint now records a WalMark in /meta (wal_applied_len/wal_applied_crc): the byte length and chained CRC of the WAL prefix it folded in. On open, if the WAL's v3 CRC chain passes through exactly that position, the entries up to it are skipped; otherwise (the normal case: the WAL was truncated) everything is replayed. No WAL format change; files without the attributes behave as before. WalFile tracks its chain length alongside running_crc and resumes both on reopen. Also make the checkpoint and snapshot durable as a unit: sync the temp file before the rename and the parent directory after it, so a power loss can't leave an empty or partial .h5 under the final name. This is per-checkpoint cost only; individual WAL appends remain unsynced by design. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
a9f78ca5a1
commit
943b9141e3
@@ -12,16 +12,36 @@ use crate::MemoryError;
|
||||
use crate::cache::MemoryCache;
|
||||
use crate::knowledge::KnowledgeCache;
|
||||
use crate::session::SessionCache;
|
||||
use crate::wal::WalMark;
|
||||
|
||||
pub const SCHEMA_VERSION: &str = "1.0";
|
||||
pub const ZEROCLAW_VERSION: &str = "0.8.0";
|
||||
|
||||
/// `/meta` attributes holding the [`WalMark`] of the WAL prefix already folded
|
||||
/// into this file. Absent on files written before the mark existed, and when
|
||||
/// the checkpoint was taken with an empty WAL.
|
||||
const WAL_APPLIED_LEN_ATTR: &str = "wal_applied_len";
|
||||
const WAL_APPLIED_CRC_ATTR: &str = "wal_applied_crc";
|
||||
|
||||
/// Build a complete HDF5 file from the in-memory state.
|
||||
pub fn build_hdf5_file(
|
||||
config: &MemoryConfig,
|
||||
cache: &MemoryCache,
|
||||
sessions: &SessionCache,
|
||||
knowledge: &KnowledgeCache,
|
||||
) -> Result<Vec<u8>, MemoryError> {
|
||||
build_hdf5_file_with_mark(config, cache, sessions, knowledge, None)
|
||||
}
|
||||
|
||||
/// [`build_hdf5_file`], recording which WAL prefix this state already
|
||||
/// contains (see [`WalMark`]) so a crash before the WAL is truncated doesn't
|
||||
/// replay those entries a second time.
|
||||
pub fn build_hdf5_file_with_mark(
|
||||
config: &MemoryConfig,
|
||||
cache: &MemoryCache,
|
||||
sessions: &SessionCache,
|
||||
knowledge: &KnowledgeCache,
|
||||
wal_applied: Option<WalMark>,
|
||||
) -> Result<Vec<u8>, MemoryError> {
|
||||
let mut builder = clawhdf5::FileBuilder::new();
|
||||
|
||||
@@ -38,6 +58,10 @@ pub fn build_hdf5_file(
|
||||
"edgehdf5_version",
|
||||
AttrValue::String(ZEROCLAW_VERSION.into()),
|
||||
);
|
||||
if let Some(mark) = wal_applied.filter(|m| m.len > 0) {
|
||||
meta.set_attr(WAL_APPLIED_LEN_ATTR, AttrValue::I64(mark.len as i64));
|
||||
meta.set_attr(WAL_APPLIED_CRC_ATTR, AttrValue::I64(i64::from(mark.crc)));
|
||||
}
|
||||
// Need at least one dataset in the group for it to be a proper group
|
||||
meta.create_dataset("_marker").with_u8_data(&[1]).compact();
|
||||
let finished_meta = meta.finish();
|
||||
@@ -309,6 +333,20 @@ fn write_string_dataset(
|
||||
}
|
||||
|
||||
/// Validate an HDF5 file has the correct schema and load all data.
|
||||
/// Read the checkpoint's [`WalMark`] from `/meta`, if it has one.
|
||||
pub fn read_wal_mark(file: &clawhdf5::File) -> Option<WalMark> {
|
||||
let attrs = file.group("meta").ok()?.attrs().ok()?;
|
||||
let len = match attrs.get(WAL_APPLIED_LEN_ATTR)? {
|
||||
AttrValue::I64(v) => u64::try_from(*v).ok()?,
|
||||
_ => return None,
|
||||
};
|
||||
let crc = match attrs.get(WAL_APPLIED_CRC_ATTR)? {
|
||||
AttrValue::I64(v) => u32::try_from(*v).ok()?,
|
||||
_ => return None,
|
||||
};
|
||||
Some(WalMark { len, crc })
|
||||
}
|
||||
|
||||
pub fn validate_and_load(
|
||||
file: &clawhdf5::File,
|
||||
) -> Result<(MemoryConfig, MemoryCache, SessionCache, KnowledgeCache), MemoryError> {
|
||||
|
||||
Reference in New Issue
Block a user