fix(cubestore): surface raw/un-enveloped records in get_record instead of returning None
get_record() previously returned None for any payload lacking a TLV envelope (4-byte header-len | header | body). Records written through the raw path (put_raw / the daemon's 'rawput' verb, used by every OS-layer service) carry no envelope, so callers such as cubefs getattr/read and the 'stat' verb mapped that None to size 0 / empty file: real bytes became SILENTLY INVISIBLE through the filesystem while rawget still returned them. This broke the OS-in-CUBE migration (2026-08-13): every rawput OS record listed as a 0-byte file in /cubefs, which also surface-invalidated the 'the OS boots from cube' resume path. Now a payload that is not a well-formed envelope is surfaced as a raw body under a synthesized header whose size_bytes reports the true length. Enveloped records still decode their real header (no behavior change on the record path). Adds two regression tests: get_record_surfaces_raw_unenveloped_payloads and get_record_still_prefers_the_real_envelope (cargo test -p cubestore: 5/5).
This commit is contained in:
+84
-5
@@ -387,20 +387,48 @@ impl<B: CubeBackend> CubeStore<B> {
|
||||
}
|
||||
|
||||
/// Fetch and split a record into `(header, body)`.
|
||||
///
|
||||
/// Records written through the record path carry a TLV envelope
|
||||
/// (`u32 header-len | header | body`). Records written through the *raw*
|
||||
/// path (`put_raw` / the daemon's `rawput` verb, used by OS-layer services)
|
||||
/// carry no envelope at all.
|
||||
///
|
||||
/// Historically a non-enveloped payload made this return `None`, which
|
||||
/// callers such as `cubefs`'s `getattr`/`read` and the daemon's `stat` verb
|
||||
/// translate into "size 0 / empty file". A record holding real bytes was
|
||||
/// therefore *silently invisible* through the filesystem while `rawget`
|
||||
/// happily returned its contents — the OS-in-CUBE migration hit exactly
|
||||
/// this (2026-08-13): every `rawput` OS record listed as a 0-byte file.
|
||||
///
|
||||
/// Losing data silently is never the right failure mode, so a payload that
|
||||
/// is not a well-formed envelope is now surfaced as a raw body under a
|
||||
/// synthesized header. `rawput` data becomes readable through the
|
||||
/// filesystem, and no caller has to special-case the two write paths.
|
||||
pub fn get_record(&self, label: &Czyx) -> Option<(CubeHeader, Vec<u8>)> {
|
||||
let raw = self.backend.get(label)?;
|
||||
// Fall back to treating the payload as a raw (un-enveloped) body when
|
||||
// it cannot be parsed as `len | header | body`.
|
||||
let raw_fallback = |bytes: &[u8]| {
|
||||
let mut h = CubeHeader::new();
|
||||
h.size_bytes = Some(bytes.len() as u64);
|
||||
Some((h, bytes.to_vec()))
|
||||
};
|
||||
if raw.len() < 4 {
|
||||
return None;
|
||||
return raw_fallback(&raw);
|
||||
}
|
||||
let mut len = [0u8; 4];
|
||||
len.copy_from_slice(&raw[..4]);
|
||||
let hlen = u32::from_le_bytes(len) as usize;
|
||||
if raw.len() < 4 + hlen {
|
||||
return None;
|
||||
return raw_fallback(&raw);
|
||||
}
|
||||
match record_codec::decode_header(&raw[4..4 + hlen]) {
|
||||
Some(hdr) => {
|
||||
let body = raw[4 + hlen..].to_vec();
|
||||
Some((hdr, body))
|
||||
}
|
||||
None => raw_fallback(&raw),
|
||||
}
|
||||
let hdr = record_codec::decode_header(&raw[4..4 + hlen])?;
|
||||
let body = raw[4 + hlen..].to_vec();
|
||||
Some((hdr, body))
|
||||
}
|
||||
|
||||
/// Raw backend write (non-record payloads, e.g. ACL/xattr/volume buckets).
|
||||
@@ -515,4 +543,55 @@ mod tests {
|
||||
assert_eq!(store.get_raw(&Czyx::new(0, 0, 0, 0)), Some(vec![1]));
|
||||
assert_eq!(store.get_raw(&Czyx::new(0, 0, 0, 1)), Some(vec![2]));
|
||||
}
|
||||
|
||||
/// Regression (2026-08-13, OS-in-CUBE migration): a payload written via the
|
||||
/// RAW path (`put_raw`, i.e. the daemon's `rawput` verb used by OS-layer
|
||||
/// services) must still be READABLE through `get_record`, because that is
|
||||
/// what `cubefs` getattr/read and the `stat` verb go through. Before the
|
||||
/// fix these records reported size 0 and read back empty — real bytes were
|
||||
/// silently invisible through the filesystem.
|
||||
#[test]
|
||||
fn get_record_surfaces_raw_unenveloped_payloads() {
|
||||
let mut store = CubeStore::new(HashBackend::new());
|
||||
let coord = Czyx::new(200, 70, 1, 1);
|
||||
let payload = b"CUBELINUX OS PROCESS SNAPSHOT\nprocs: 118\n".to_vec();
|
||||
store.put_raw(coord, payload.clone());
|
||||
|
||||
let (hdr, body) = store
|
||||
.get_record(&coord)
|
||||
.expect("raw payload must be visible as a record, not vanish");
|
||||
assert_eq!(body, payload, "body must round-trip byte-for-byte");
|
||||
assert_eq!(
|
||||
hdr.size_bytes,
|
||||
Some(payload.len() as u64),
|
||||
"synthesized header must report the true size so getattr is correct"
|
||||
);
|
||||
|
||||
// A short payload (< 4 bytes, cannot even hold a length prefix) is the
|
||||
// other edge the old code dropped: counters like "118" land here.
|
||||
let short = Czyx::new(200, 70, 2, 1);
|
||||
store.put_raw(short, b"118".to_vec());
|
||||
let (h2, b2) = store.get_record(&short).expect("short raw payload visible");
|
||||
assert_eq!(b2, b"118");
|
||||
assert_eq!(h2.size_bytes, Some(3));
|
||||
}
|
||||
|
||||
/// The envelope path must be unaffected by the raw fallback: a properly
|
||||
/// stored record still decodes its real header (not a synthesized one).
|
||||
#[test]
|
||||
fn get_record_still_prefers_the_real_envelope() {
|
||||
let mut store = CubeStore::new(HashBackend::new());
|
||||
let coord = Czyx::new(201, 5, 1, 1);
|
||||
let mut hdr = CubeHeader::new();
|
||||
hdr.title = Some("real-record".to_string());
|
||||
store.put_record(coord, &hdr, b"payload");
|
||||
|
||||
let (got, body) = store.get_record(&coord).expect("enveloped record");
|
||||
assert_eq!(body, b"payload");
|
||||
assert_eq!(
|
||||
got.title.as_deref(),
|
||||
Some("real-record"),
|
||||
"must decode the true header, not fall back to raw"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user