perf(delivery): balance verified restore byte groups

This commit is contained in:
2026-10-28 18:28:06 +08:00
parent 70007a36b9
commit 60a0dad94f
4 changed files with 121 additions and 16 deletions
@@ -87,9 +87,9 @@ def test_lexical_cache_is_bounded_and_custom_hash_cannot_bypass_policy():
with pytest.raises(policy.DeliveryError, match="private or unsafe"):
policy.relative_name(Collision("backend/../escape"))
assert policy._cached_relative_name.cache_info() == before
for index in range(9000):
for index in range(20000):
policy.relative_name("backend/member-" + str(index))
assert policy._cached_relative_name.cache_info().currsize == 8192
assert policy._cached_relative_name.cache_info().currsize == 16384
finally:
policy._cached_relative_name.cache_clear()
+112 -11
View File
@@ -29,6 +29,29 @@ fn remaining(deadline: u64) -> Result<()> {
Ok(())
}
// Preserve contiguous hash/path order while minimizing the heavier byte group.
// Equal-byte choices prefer the original balanced file count (including zeroes).
fn balanced_split(rows: &[restore_io::RestoreFile]) -> usize {
if rows.len() <= 1 {
return rows.len();
}
let middle = rows.len().div_ceil(2);
let total: u64 = rows.iter().map(|row| row.bytes).sum();
let mut prefix = 0;
let mut best = (u64::MAX, usize::MAX);
let mut chosen = middle;
for (index, row) in rows.iter().take(rows.len() - 1).enumerate() {
prefix += row.bytes;
let split = index + 1;
let cost = (prefix.max(total - prefix), split.abs_diff(middle));
if cost < best {
best = cost;
chosen = split;
}
}
chosen
}
enum Job {
Copy(Vec<restore_io::RestoreFile>, u64),
Read(Vec<Pending>, u64),
@@ -89,18 +112,18 @@ impl Pool {
roots: &Roots,
deadline: u64,
) -> Result<Vec<Pending>> {
let size = rows.len().div_ceil(2);
let mut groups = vec![Vec::new(), Vec::new()];
for (i, row) in rows.into_iter().enumerate() {
groups[i / size].push(row);
}
let count = rows.len();
let split = balanced_split(&rows);
let mut rows = rows;
let second = rows.split_off(split);
let groups = [rows, second];
for (worker, group) in self.workers.iter().zip(groups) {
worker
.sender
.send(Job::Copy(group, deadline))
.map_err(|_| Error::Filesystem)?;
}
let mut pending = Vec::with_capacity(size * 2);
let mut pending = Vec::with_capacity(count);
let mut error = None;
// Observe both completions even when the first failed. No sync/readback
// and no success response can outrun an outstanding copy worker.
@@ -119,17 +142,15 @@ impl Pool {
return Err(e);
}
roots.sync(deadline)?;
let mut groups = vec![Vec::new(), Vec::new()];
for (i, row) in pending.into_iter().enumerate() {
groups[i / size].push(row);
}
let second = pending.split_off(split);
let groups = [pending, second];
for (worker, group) in self.workers.iter().zip(groups) {
worker
.sender
.send(Job::Read(group, deadline))
.map_err(|_| Error::Filesystem)?;
}
let mut verified = Vec::with_capacity(size * 2);
let mut verified = Vec::with_capacity(count);
for worker in &self.workers {
match worker.receiver.recv() {
Ok(Completed::Read(Ok(rows))) => verified.extend(rows),
@@ -386,6 +407,86 @@ mod tests {
roots.unchanged().unwrap_or_else(|_| panic!("root anchors"));
}
fn weighted_rows(weights: &[u64]) -> Vec<restore_io::RestoreFile> {
weights
.iter()
.enumerate()
.map(|(index, &bytes)| restore_io::RestoreFile {
path: format!("artifacts/file-{index}"),
uid: 1000,
gid: 1000,
mode: 0o644,
bytes,
sha256: "0".repeat(64),
})
.collect()
}
#[test]
fn every_small_weight_inventory_has_minimal_contiguous_heavier_group() {
assert_eq!(balanced_split(&[]), 0);
assert_eq!(balanced_split(&weighted_rows(&[9])), 1);
assert_eq!(balanced_split(&weighted_rows(&[0; 5])), 3);
assert_eq!(balanced_split(&weighted_rows(&[1, 1, 1, 20])), 3);
assert_eq!(balanced_split(&weighted_rows(&[20, 1, 1, 1])), 1);
for count in 2u32..=6 {
for mut encoded in 0..3usize.pow(count) {
let weights: Vec<_> = (0..count)
.map(|_| {
let weight = [0, 1, 13][encoded % 3];
encoded /= 3;
weight
})
.collect();
let rows = weighted_rows(&weights);
let split = balanced_split(&rows);
assert!(split > 0 && split < rows.len());
let cost = |at| {
let (a, b) = weights.split_at(at);
a.iter().sum::<u64>().max(b.iter().sum())
};
assert!((1..rows.len()).all(|other| cost(split) <= cost(other)));
}
}
}
#[test]
fn skewed_groups_keep_every_copy_sync_readback_and_final_witness() {
for weights in [[64, 64, 64, 4096], [4096, 64, 64, 64]] {
let fixture = files::Fixture::new();
let roots = Arc::new(fixture.roots());
let mut rows = weighted_rows(&weights);
use std::os::unix::fs::MetadataExt;
let owner = fs::metadata(fixture.source()).unwrap();
for row in &mut rows {
let data = vec![b'x'; row.bytes as usize];
row.sha256 = format!("{:x}", Sha256::digest(&data));
row.uid = owner.uid();
row.gid = owner.gid();
let source = fixture.source().join(&row.sha256);
fs::write(&source, &data).unwrap();
fs::set_permissions(&source, fs::Permissions::from_mode(0o600)).unwrap();
}
let pool = Pool::new(Arc::clone(&roots)).unwrap_or_else(|_| panic!("pool"));
let verified = pool
.materialize(rows, &roots, now().unwrap() + 10_000_000_000)
.unwrap_or_else(|_| panic!("skewed verified copies"));
assert_eq!(verified.len(), weights.len());
for row in &verified {
roots
.final_file(row)
.unwrap_or_else(|_| panic!("final witness"));
let name = row.row.path.strip_prefix("artifacts/").unwrap();
assert_eq!(
fs::read(fixture.target().join(name)).unwrap(),
vec![b'x'; row.row.bytes as usize]
);
}
drop(pool);
roots.unchanged().unwrap_or_else(|_| panic!("root anchors"));
}
}
#[test]
fn request_line_requires_finite_complete_frame() {
assert!(matches!(
+5 -1
View File
@@ -478,7 +478,7 @@ time from a locked iterator; there is no Future for each file or eagerly queued
inventory. Failure or deadline expiry stops dispatch, and all running calls join
before an error or result returns. Results retain the input order.
Payload path spelling checks use the same immutable allowlist and may retain at
most 8192 successful, length-bounded strings per process. Custom string subclasses
most 16384 successful, length-bounded strings per process. Custom string subclasses
bypass this cache. Byte reads, filesystem topology, ownership and all metadata
checks remain live; no filesystem or execution result enters the lexical cache.
@@ -511,6 +511,10 @@ directory finalization and success receipts wait for all dispatched work. On
failure, queued work is cancelled and running calls are joined before the error
returns. Private partial data is retained for inspection. The original operator
CPU, memory, PID and deadline limits still apply.
The native restore helper keeps two joined threads and partitions each fixed batch
into contiguous groups that minimize the heavier byte count. Ties prefer balanced
file counts. Copy order, synchronization, complete byte readback and final
nine-field witnesses are preserved; no extra worker or batch capacity is added.
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
+2 -2
View File
@@ -98,8 +98,8 @@ def _relative_name(value: str) -> str:
# Only immutable lexical policy results are reused. No byte, path existence,
# metadata, ownership, topology or execution decision is cached here. At most
# 8192 already length-bounded strings survive in one process.
_cached_relative_name = lru_cache(maxsize=8192)(_relative_name)
# 16384 already length-bounded strings survive in one process.
_cached_relative_name = lru_cache(maxsize=16384)(_relative_name)
def relative_name(value: Any) -> str: