Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/lib/tests/export_stream_memory.rs

//! ★ **RA1-13: the bundle export door streams, it does not hold the bundle twice.**
//!
//! `BundleService.Export` answers an airgap bundle candidate (an OCI bundle is gigabytes).
//! The door used to make EVERY chunk up front (a copy of the whole bundle, collected into a
//! Vec) while still holding the member map: the bundle twice in memory before a byte left.
//! `grpc::doors::export_stream` owns the members and makes each chunk only when the stream is
//! polled, and frees a member once it is sent.
//!
//! This binary counts every live heap byte (its own global allocator) and streams a 192 MiB
//! candidate: the extra memory while it streams stays within a few chunks, and every byte
//! still arrives, in order, member by member, then the summary.

use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};

use holger_server_lib::grpc::doors::{export_stream, EXPORT_CHUNK_BYTES};
use holger_server_lib::grpc::holger_doors_proto as pb;

struct Counting;
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Counting {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let p = unsafe { System.alloc(layout) };
        if !p.is_null() {
            let now = LIVE.fetch_add(layout.size(), Ordering::SeqCst) + layout.size();
            PEAK.fetch_max(now, Ordering::SeqCst);
        }
        p
    }
    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        unsafe { System.dealloc(ptr, layout) };
        LIVE.fetch_sub(layout.size(), Ordering::SeqCst);
    }
}

#[global_allocator]
static A: Counting = Counting;

const MIB: usize = 1024 * 1024;

#[test]
fn a_large_bundle_streams_in_bounded_memory() {
    // A 192 MiB candidate: three 64 MiB payloads and a small evidence member.
    let mut members = std::collections::BTreeMap::new();
    for (i, name) in ["payload/a.tar", "payload/b.tar", "payload/c.tar"]
        .iter()
        .enumerate()
    {
        members.insert(name.to_string(), vec![i as u8 + 1; 64 * MIB]);
    }
    members.insert("evidence/sbom.json".to_string(), b"{}".to_vec());
    let total: usize = members.values().map(Vec::len).sum();
    let summary = pb::ExportBundleSummary {
        bundle_id: "big".into(),
        members: 4,
        ..Default::default()
    };

    let held = LIVE.load(Ordering::SeqCst);
    PEAK.store(held, Ordering::SeqCst);
    let mut got = 0usize;
    let mut order: Vec<String> = Vec::new();
    let mut last_summary = None;
    let mut after_a = usize::MAX;
    for c in export_stream(members, EXPORT_CHUNK_BYTES, summary.clone()) {
        assert!(
            c.data.len() <= EXPORT_CHUNK_BYTES,
            "a chunk is at most one chunk"
        );
        if let Some(first) = c.data.first().filter(|_| c.path.starts_with("payload/")) {
            assert!(
                c.data.iter().all(|b| b == first),
                "a chunk is its member's bytes"
            );
        }
        got += c.data.len();
        if order.last() != Some(&c.path) {
            if c.path == "payload/b.tar" {
                after_a = LIVE.load(Ordering::SeqCst);
            }
            order.push(c.path.clone());
        }
        last_summary = c.summary.clone();
        // the chunk is dropped here, as tonic drops it once it is on the wire
    }
    let extra = PEAK.load(Ordering::SeqCst).saturating_sub(held);
    assert_eq!(got, total, "every byte arrives");
    assert_eq!(
        order,
        [
            "evidence/sbom.json",
            "payload/a.tar",
            "payload/b.tar",
            "payload/c.tar",
            ""
        ],
        "member by member, then the summary"
    );
    assert_eq!(last_summary, Some(summary), "the summary comes last");
    assert!(
        extra <= 8 * EXPORT_CHUNK_BYTES,
        "streaming a {} MiB bundle took {} MiB more than the bundle itself: it must stay within a few chunks",
        total / MIB,
        extra / MIB
    );
    assert!(
        after_a < held - 32 * MIB,
        "a member is freed once it is sent: {} MiB live at payload/b.tar, {} MiB before",
        after_a / MIB,
        held / MIB
    );
}