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:
CUBELinux-2
2026-08-13 17:29:50 -04:00
parent 3121459138
commit ff2914a105
+84 -5
View File
@@ -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"
);
}
}