perf(delivery): reduce redundant restore file syscalls

This commit is contained in:
2026-10-28 23:15:32 +08:00
parent a436c659e9
commit 4e5f491952
3 changed files with 112 additions and 8 deletions
+13 -8
View File
@@ -266,22 +266,20 @@ impl Roots {
{
return Err(Error::Identity);
}
let current = output.metadata().map_err(|_| Error::Filesystem)?;
let changed = (current.uid(), current.gid()) != (uid, gid);
// This descriptor was exclusively created by this copy. Its initial
// owner/mode decides the required changes; the final fstat below still
// checks the complete resulting file after every write and mode change.
let changed = (empty.uid(), empty.gid()) != (uid, gid);
if changed {
rustix::fs::fchown(&output, Some(Uid::from_raw(uid)), Some(Gid::from_raw(gid)))
.map_err(|_| Error::Filesystem)?;
}
let mode_changed = changed || current.mode() & 0o7777 != mode;
let mode_changed = changed || empty.mode() & 0o7777 != mode;
if mode_changed {
rustix::fs::fchmod(&output, Mode::from_raw_mode(mode))
.map_err(|_| Error::Filesystem)?;
}
let final_info = if mode_changed {
output.metadata().map_err(|_| Error::Filesystem)?
} else {
current
};
let final_info = output.metadata().map_err(|_| Error::Filesystem)?;
member(&final_info, &row, uid, gid, mode)?;
let witness = stamp(&final_info)?;
drop(output);
@@ -458,6 +456,13 @@ fn verify(
data.extend_from_slice(&buffer[..count]);
}
total = end;
// A short read already delivered every declared byte. The caller
// checks the same descriptor/path full nine-field witness (including
// exact size) before and after verification, so a changed or longer
// file still fails. Exact buffer multiples still require an explicit EOF read.
if total as u64 == row.bytes && count < buffer.len() {
break;
}
}
if total as u64 != row.bytes
|| hasher.is_some_and(|h| format!("{:x}", h.finalize()) != row.sha256)
@@ -527,6 +527,97 @@ mod tests {
roots.unchanged().unwrap_or_else(|_| panic!("root anchors"));
}
#[test]
fn exact_lengths_and_unchanged_create_mode_keep_complete_readback() {
let fixture = files::Fixture::new();
let roots = Arc::new(fixture.roots());
use std::os::unix::fs::MetadataExt;
let owner = fs::metadata(fixture.source()).unwrap();
let mut rows = Vec::new();
for (index, size) in [0, 13, 1024 * 1024, 1024 * 1024 + 13]
.into_iter()
.enumerate()
{
let mut data = vec![b'a'; size];
if let Some(last) = data.last_mut() {
*last = b'z';
}
let hash = format!("{:x}", Sha256::digest(&data));
fs::write(fixture.source().join(&hash), &data).unwrap();
fs::set_permissions(
fixture.source().join(&hash),
fs::Permissions::from_mode(0o600),
)
.unwrap();
rows.push(restore_io::RestoreFile {
path: format!("artifacts/length-{index}"),
uid: owner.uid(),
gid: owner.gid(),
mode: 0o600,
bytes: size as u64,
sha256: hash,
});
}
let pool = Pool::new(Arc::clone(&roots)).unwrap_or_else(|_| panic!("pool"));
for _ in 0..2 {
let verified = pool
.materialize(rows.clone(), &roots, now().unwrap() + 10_000_000_000)
.unwrap_or_else(|_| panic!("complete lengths including existing files"));
assert_eq!(verified.len(), 4);
for pending in &verified {
roots
.final_file(pending)
.unwrap_or_else(|_| panic!("final witness"));
let name = pending.row.path.strip_prefix("artifacts/").unwrap();
let data = fs::read(fixture.target().join(name)).unwrap();
assert_eq!(data.len() as u64, pending.row.bytes);
assert_eq!(format!("{:x}", Sha256::digest(&data)), pending.row.sha256);
assert_eq!(
fs::metadata(fixture.target().join(name))
.unwrap()
.permissions()
.mode()
& 0o7777,
0o600
);
}
}
drop(pool);
roots.unchanged().unwrap_or_else(|_| panic!("root anchors"));
}
#[test]
fn short_final_chunk_still_refuses_a_wrong_last_byte() {
for size in [13, 1024 * 1024 + 13] {
let fixture = files::Fixture::new();
let roots = Arc::new(fixture.roots());
let mut data = vec![b'a'; size];
let expected = format!("{:x}", Sha256::digest(&data));
*data.last_mut().unwrap() = b'z';
let source = fixture.source().join(&expected);
fs::write(&source, data).unwrap();
fs::set_permissions(&source, fs::Permissions::from_mode(0o600)).unwrap();
use std::os::unix::fs::MetadataExt;
let owner = fs::metadata(&source).unwrap();
let pool = Pool::new(Arc::clone(&roots)).unwrap_or_else(|_| panic!("pool"));
assert!(matches!(
pool.materialize(
vec![restore_io::RestoreFile {
path: "artifacts/wrong-last-byte".into(),
uid: owner.uid(),
gid: owner.gid(),
mode: 0o600,
bytes: size as u64,
sha256: expected,
}],
&roots,
now().unwrap() + 10_000_000_000
),
Err(Error::Bytes)
));
}
}
#[test]
fn request_line_requires_finite_complete_frame() {
assert!(matches!(
+8
View File
@@ -520,6 +520,14 @@ kernel to release clean cached pages for that restored file. Descriptor and path
witnesses are still checked afterwards. This per-file streaming hint avoids
retaining historical file contents in the fixed restore memory budget; it does
not change file bytes, durability, limits or the live filesystem checks.
For an exclusively created destination, its initial descriptor owner and mode
select the necessary changes; an unconditional final descriptor check still
confirms the complete file size, owner, mode and all nine identity fields.
A short read that supplies every declared byte completes hashing or byte equality
without another EOF-only read. Full descriptor/path size and identity checks
still surround the readback; empty files and exact buffer multiples retain an
explicit EOF read. Source EOF checks, synchronization and final all-file witnesses
are unchanged.
Old configuration and maintenance receipts go into inactive
`recovery/ID/history/`. The new boot's Sandbox preparation owns cgroup recreation.
Unknown/nonempty targets and differing existing files are refused; partial restores