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

nordisk/znippy

.nornir/slot-pool-memory-plan.md

PLAN — bound the compress slot-pool memory (Njord backup, issue #23)

Status: §1 IMPLEMENTED (2026-07-22, oden) · §2 (disk-backed slots) still proposed

2026-08-25 — the pool moved out from under this document. Everything below that names Magazine / slotpool.rs describes a fork znippy no longer owns: the slot pool is now gatling::revolver in coalescing mode (znippy_zoomies::chunk_revolver), and only the CUT POLICY stayed, as znippy-common/src/poolplan.rs. PoolPlan::plan and every bound §1 describes are unchanged and still hold — read Magazine::new as ChunkRevolver::with_coalescing and slotpool.rs as poolplan.rs throughout.

§2 is UNAFFECTED and still open. Disk-backed slots change where a slot's bytes live, not how slots are claimed, packed or released, so the collapse neither blocks nor advances it — but it must now be built behind the upstream Filler/PackedSlice surface, and therefore belongs upstream too, so no consumer changes.

What landed

The 1.6 GiB unconditional reservation is gone. slot_packer.rs no longer holds SLOT_SIZE/NUM_SLOTS consts; each pass plans its pool with slotpool::PoolPlan::plan(input_bytes, num_workers, budget), which reserves the smallest of three bounds — the old default (never more than 8 × 200 MiB), the input (ceil(input / slice_size) slices; a 5 MiB tree reserves 7 MiB, not 1.6 GiB), and the budget (common_config::slot_pool_budget_bytes(): ZNIPPY_SLOT_POOL_BYTES, else 50 % of the cgroup headroom, else 50 % of host available memory). Floor: one slot of one slice, so the pipeline always makes progress.

Deviation from the plan below, and the reason: the plan proposed shrinking slot_size, from which Magazine::new derives slice_size. slice_size is output-affecting — it is the big/small partition threshold and the big-file cut length, so it lands in chunk_seq, fdata_offset and every chunk checksum. Shrinking it under memory pressure would have made the archive depend on how much RAM the pod had. So Magazine gained with_slice_size/from_plan, slice_size stays pinned at 200 MiB / num_workers regardless of the plan, and only the reservation moves. slot_size × num_slots affects nothing but how many slices coalesce per buffer and how many buffers are in flight; the ordered sink writes in producer order either way.

Measured on oden (32 cores, 29 workers → slice_size 7 231 558 B), debug build:

input budget pool reserved peak RSS
2.4 GB default 8 × 209 715 200 = 1.6 GiB (the historical geometry, exactly) 2 741 MB
765 MB default 8 × 94 010 254 = 752 MB (input-bound) 1 852 MB
765 MB ZNIPPY_SLOT_POOL_BYTES=64MiB 8 × 7 231 558 = 57.9 MB 1 165 MB
765 MB ZNIPPY_SLOT_POOL_BYTES=16MiB 2 × 7 231 558 = 14.5 MB 1 130 MB
5 MB default 1 × 7 231 558 = 7.2 MB 61 MB

No fat-path regression by construction: when neither the input nor the budget binds, plan early-returns PoolPlan::default_for(num_workers) — the same 8 × 200 MiB the consts gave — as the 2.4 GB row shows.

Still open after this. The slot pool is no longer the RAM floor, but it was not the only one: the 765 MB run still peaks at ~1.13 GB with a 14 MB pool. The residual is the per-worker codec state — 29 gatling workers × a zstd level-19 context (CONFIG.compression_level = 19) plus zstd_output_buffer_size scratch. On a small pod max_core_in_flight scales down with the core count so this shrinks with it, but if a backup pod is still tight, that is the next term to bound, not the slot pool.

Original plan follows.

Status (original): proposed (not yet implemented) Driver: Njord issue #23 "Bygg en stabil IT-arkitektur 2.0". The self-built backup/restore (tillsynia-admin) seals its staging tree with znippy as a lib (znippy_compress::compress_dir). Ops flagged that the backup "has previously needed a lot of memory because the snapshot is handled in memory". The Njord side is now fully streaming (S3 blobs → disk, DB COPY chunked, uploads multipart), so the remaining RAM floor is znippy's slot pool.

The problem

znippy-compress/src/slot_packer.rs hardcodes:

const SLOT_SIZE: usize = 200 * 1024 * 1024; // 200 MiB
const NUM_SLOTS: usize = 8;

Magazine::new (znippy-common/src/slotpool.rs) allocates the whole pool eagerly, up front, regardless of input size:

.map(|_| vec![0u8; slot_size].into_boxed_slice())  // × num_slots

So every compress_dir call reserves 8 × 200 MiB = 1.6 GiB of heap before touching a single file, plus per-worker zstd/OpenZL scratch buffers. On a backup pod with a modest memory limit (e.g. 512 MiB–1 GiB request) this OOM-kills or forces a large, wasteful reservation. The pool size is independent of the data volume — a 5 MiB staging dir still reserves 1.6 GiB.

Crucially, StrategicConfig (znippy-common/src/common_config.rs) already computes max_mem_allowed and min_free_memory_ratio from detected RAM — but SLOT_SIZE/NUM_SLOTS ignore it. The knobs exist; the slot pool just doesn't read them.

Goals

  1. Cap slot-pool RAM to a caller/config-chosen budget (target: a backup can run comfortably under ~256 MiB total).
  2. Offer a disk-backed slot mode ("use tmp disk rather than RAM") for memory-constrained hosts, trading throughput for a tiny RSS.
  3. No throughput regression on the fat/bench path (Loki, Odin) when memory is plentiful — the current 1.6 GiB behaviour stays the default there.
  4. Preserve the zero-copy skip/incompressible path and the two-pass (big/small) design.

Proposed changes (in priority order)

1. Make the pool config-driven (cheap, high value)

2. Disk-backed slots (tmpfs/tmp-disk mode)

3. Report the reservation

Tests (constellation testable-code law)

Out of scope

Njord side — already done (no znippy dependency)

tillsynia-admin now streams everywhere it previously buffered: ObjStore::get_to_file / put_file / get_stream (multipart up, chunked down), restore DB load via chunked COPY FROM STDIN, and the admin-server snapshot up/download endpoints stream instead of read-into-Vec. That removes the per-object and per-bundle RAM spikes; this plan removes the last floor.