risingwavelabs/risingwave · error · BackupError
unexpected bytes after meta snapshot checksum
Error message
unexpected bytes after meta snapshot checksum
What it means
MetaSnapshot::finish verifies the trailing checksum then reads one extra byte to confirm nothing follows it; if the reader still returns bytes it throws "unexpected bytes after meta snapshot checksum". The snapshot stream contains trailing data beyond the defined format, so the object is not a well-formed meta snapshot.
Solutions
- Upgrade the RisingWave/risectl binary to a version matching (or newer, with V2 section skipping) the backup producer before restoring.
- Verify the snapshot object is exactly the writer's output (compare byte length/checksum from the backup manifest).
- Recreate the backup without post-processing (no compression-on-write tricks, no concatenation).
- If intentional forward-compat data is present, port the reader to skip unknown trailing sections like decode_hummock_sequences_from_stream does.
Example fix
// before: strict full read
snapshot.finish().await?;
// after: skip unknown trailing sections when tolerated, or ensure exact match
let n = reader.read(&mut trailing).await?;
if n != 0 && !allow_trailing { snapshot.finish().await?; } Defensive patterns
Strategy: try-catch
Validate before calling
let size = storage.get_object_size(&key).await?;
let expected = manifest.snapshot_size; // written at backup time
if size != expected {
return Err(anyhow!("snapshot size mismatch: {} != {}", size, expected));
} Type guard
fn is_exact_size(actual: u64, expected: u64) -> bool { actual == expected } Try / catch
match snapshot.finish().await {
Ok(()) => {},
Err(e) if e.to_string().contains("unexpected bytes after") => {
log::warn!("trailing data in snapshot; producer likely newer version");
// handle per policy: upgrade reader, or reject
}
Err(e) => return Err(e.into()),
} Prevention
- Keep the restoring binary version >= the version that produced the backup.
- Never post-process or append to snapshot objects after writing.
- Compare restored object byte length to the manifest before decoding.
When it happens
Trigger: Calling finish() on a reader where bytes remain after the checksum — e.g. the snapshot object has appended/future-incompatible sections written by a newer tool, or the object wraps the snapshot with extra framing/headers.
Common situations: A newer RisingWave version wrote extra sections into the snapshot and an older binary tries to read it; an export script concatenated metadata after the snapshot; double-reading a stream that was not consumed exactly once; manual edits to a backup file.
Related errors
- unsupported metadata snapshot format version for hummock…
- BackupStorage error
- Checksum mismatch: expected
- concurrent backup job is not supported: existent job
- Decoding error
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/d10b5b203ef9ce05.
Report an issue: GitHub.
Appendix: source
Thrown at src/storage/backup/src/meta_snapshot.rs:236
if tail.len() != size_of::<u64>() {
return Err(BackupError::Decoding(
anyhow::anyhow!("meta snapshot is missing checksum").into(),
));
}
Self::verify_checksum_with_hasher(&hasher, &tail)
}
pub async fn finish(mut self) -> BackupResult<()> {
let mut checksum = [0; size_of::<u64>()];
self.reader.read_exact(&mut checksum).await?;
self.verify_checksum(&checksum)?;
let mut trailing = [0; 1];
let n = self.reader.read(&mut trailing).await?;
if n != 0 {
return Err(BackupError::Decoding(
anyhow::anyhow!("unexpected bytes after meta snapshot checksum").into(),
));
}
Ok(())
}
fn verify_checksum(&self, checksum: &[u8]) -> BackupResult<()> {
Self::verify_checksum_with_hasher(&self.hasher, checksum)
}
fn verify_checksum_with_hasher(hasher: &XxHash64, checksum: &[u8]) -> BackupResult<()> {
let expected = u64::from_le_bytes(checksum.try_into().expect("u64 length"));
let found = hasher.finish();
if expected != found {
return Err(BackupError::ChecksumMismatch { expected, found });
}
Ok(())
}
}View on GitHub (pinned to 6469eb736d)