Skip to main content

znippy_plugin_git/
uring_write.rs

1//! Impl 3 — the io_uring arm: **one submission, four ops, kernel-enforced
2//! ordering**.
3//!
4//! [`SafeWriter`](crate::archive_write::SafeWriter) buys its durability with
5//! four blocking syscalls and four round-trips through the scheduler:
6//!
7//! ```text
8//!   pwrite(blob)  fsync(blob)  write(journal)  fsync(journal)
9//!      ↑ ring 0      ↑ ring 0       ↑ ring 0        ↑ ring 0     4 syscalls
10//! ```
11//!
12//! Here the same four operations are **one** `io_uring_enter(2)`. Each of the
13//! first three SQEs carries `IOSQE_IO_LINK`, so the kernel will not start an op
14//! until its predecessor has completed — the ordering contract is enforced by
15//! the kernel rather than by the caller blocking between calls:
16//!
17//! ```text
18//!   [ Write(blob) ]→[ Fsync(blob) ]→[ WriteFixed(journal) ]→[ Fsync(journal) ]
19//!     IO_LINK         IO_LINK          IO_LINK                (chain end)
20//!   ────────────────────── one io_uring_enter ──────────────────────
21//! ```
22//!
23//! That is **the same ordering as `SafeWriter`**, not a second one: blob bytes
24//! durable before the row that references them. A crash anywhere in the chain
25//! leaves orphan payload nobody points at. If a link fails the kernel cancels
26//! the rest of the chain with `ECANCELED`, which is exactly the semantics
27//! wanted: no journal row is ever written after a failed blob fsync.
28//!
29//! # Registered buffers, and the copy that is *not* being avoided
30//!
31//! `register_buffers` pins a set of stable buffers once, so per-op the kernel
32//! skips `get_user_pages`/`put_page` on them. That only works for buffers whose
33//! address the ring can be told about **up front**, which the caller's transient
34//! `&[u8]` is not. So this writer splits the two:
35//!
36//! * **journal** → a registered one-page staging buffer and `WriteFixed`. This is
37//!   the buffer that is stable across appends, and it is where the win is real.
38//!   Crucially it means there is **no arrow `BufWriter`** here at all: the
39//!   serialized IPC message is written into the registered buffer and the kernel
40//!   is handed that. You cannot have both arrow's buffered `StreamWriter` and a
41//!   registered buffer; the brief says take the io_uring side, and this does.
42//! * **blob** → plain `Write` against the caller's own pointer. Not registered
43//!   (it cannot be), but also **not copied** in userspace: the pushed pack bytes
44//!   go from the caller's slice straight into the ring.
45//!
46//! Saying this plainly matters: the registered buffer removes per-op page
47//! pinning on the journal, not a `memcpy` on the pack.
48//!
49//! # What a registration COSTS, and why the staging buffer is one page
50//!
51//! `IORING_REGISTER_BUFFERS` pins its pages against **`RLIMIT_MEMLOCK`**, and
52//! the kernel charges them to `user->locked_vm` — a counter kept on the
53//! `user_struct`, so it is **per-UID and shared by every process that user is
54//! running**, not per-process and not per-ring
55//! (`io_uring/rsrc.c: io_account_mem -> __io_account_mem`).
56//!
57//! Every store gets its own writer, therefore its own ring, therefore its own
58//! registration. So the pinned pages are `stores × ceil(JOURNAL_STAGING /
59//! PAGE_SIZE)` and the ceiling is a **hard, shared, uid-wide** one. The default
60//! on oden (and on stock Debian/Ubuntu) is 8 MiB.
61//!
62//! MEASURED on oden 2026-08-14, `RLIMIT_MEMLOCK` 8 MiB, one fresh process per
63//! reading, three readings each, identical code with only the buffer size
64//! varied:
65//!
66//! ```text
67//!   registered per ring   rings that fit        what refused the next one
68//!   ───────────────────   ──────────────        ─────────────────────────
69//!   64 KiB (16 pages)     87, 88, 87            IORING_REGISTER_BUFFERS
70//!    4 KiB  (1 page)      397, 422, 419         io_uring_setup
71//! ```
72//!
73//! with
74//!
75//! ```text
76//!   uring: register_buffers: Cannot allocate memory (os error 12)
77//! ```
78//!
79//! and every store opened after that refused. **~87 io_uring stores per
80//! process**, and gunnar's `multiuser_scaling` alone provisions 102 users with a
81//! repository each; a `gunnar serve` holds one store — therefore one ring,
82//! therefore one registration — per repository it has open. `FastWriter` and
83//! `SafeWriter` register nothing and have no such ceiling, which is exactly the
84//! shape the sweep showed: four workloads and eight forge arms red on **both**
85//! io_uring columns and green on `fast` and `safe`, with the index arm varied
86//! underneath and making no difference.
87//!
88//! Cutting the registration to one page moves that to **~400 rings, a 4.6×
89//! ceiling** — and past it the thing that refuses is no longer the registration
90//! at all but `io_uring_setup`. A bare ring of [`RING_ENTRIES`] costs about 21 KiB
91//! of the same budget (8 MiB / ~400), so the old writer spent ~21 pages per store
92//! and the new one spends ~6. It is not the 16× the buffer sizes suggest, because
93//! the ring was always paying five pages of it.
94//!
95//! End to end rather than at the writer: **150 whole `GitStore`s on this arm,
96//! open at once in one process**, each with a real 5 653 302-byte / 2 687-object
97//! pack pushed, indexed and read back — 3.4–4.0 s per store, oden 2026-08-14,
98//! load average 15–30. Every one of them past the old ceiling.
99//!
100//! # …and every number in the two paragraphs above was measured in the WRONG
101//! # PROCESS: the page you register is charged as the huge page it sits in
102//!
103//! *Found 2026-08-14, after the ceiling above had already been "fixed" once.*
104//!
105//! **`io_buffer_account_pin` does not charge the pages you named. It charges the
106//! `compound_head` of each of them.** If the 4 KiB you register happens to live
107//! inside a transparent huge page, the kernel charges **the whole 2 MiB — 512
108//! pages, not one** (`io_uring/rsrc.c: io_buffer_account_pin`, the
109//! `PageCompound` branch: `imu->acct_pages += page_size(hpage) >> PAGE_SHIFT`).
110//!
111//! MEASURED on oden 2026-08-14, `RLIMIT_MEMLOCK` 8 MiB, `transparent_hugepage
112//! = [madvise]`, one registration of exactly one page, charge read back off the
113//! kernel by binary-searching what could still be registered afterwards:
114//!
115//! ```text
116//!   where the one page came from                       pages charged
117//!   ────────────────────────────────────────────────   ─────────────
118//!   its own anonymous mmap, MADV_NOHUGEPAGE                        1
119//!   glibc malloc(4096) in a small C program                        2
120//!   inside a MADV_HUGEPAGE arena                                 512
121//! ```
122//!
123//! `Box<[u8]>` — what this file registered until now — is *whatever the process
124//! allocator gives you*, and **`gunnar serve` runs on mimalloc**, which
125//! `madvise(MADV_HUGEPAGE)`s its arenas. Measured on the running server:
126//! `AnonHugePages: 16384 kB` in one VMA, and every staging buffer allocated out
127//! of it. So the real ceiling in the process that matters was not ~400 stores.
128//! It was **three**:
129//!
130//! ```text
131//!   RLIMIT_MEMLOCK    io_uring stores one `gunnar serve` could open
132//!   ──────────────    ─────────────────────────────────────────────
133//!   8 MiB                             3   (+ the control store = 4 × 2 MiB)
134//!   4 MiB                             1
135//! ```
136//!
137//! — measured by pushing to distinct repositories one at a time until the server
138//! refused, oden 2026-08-14. Four huge pages fit in 8 MiB and that is the whole
139//! arithmetic. The fourth push onwards died with
140//! `register_buffers (4096 B, 1 page(s)): Cannot allocate memory`, which reads
141//! like the ceiling this file already documents and is a different one: it is not
142//! how MANY pages are registered, it is WHOSE page each one is.
143//!
144//! That is why [`Staging`] does not ask the allocator for the buffer. It takes
145//! **its own one-page anonymous mapping and `madvise(MADV_NOHUGEPAGE)`s it**, so
146//! the registration is charged one page whatever the executable's allocator does
147//! — and the "~400 rings" arithmetic above becomes true instead of merely
148//! plausible. `enough_uring_writers_for_a_population_coexist_in_one_process`
149//! could never have caught this: a `cargo test` binary is on the system
150//! allocator, whose 4 KiB allocations are not huge-page backed, so the guard
151//! measured a process that did not have the bug. The guard that does catch it is
152//! [`tests::a_registration_costs_one_page_and_not_the_huge_page_it_might_sit_in`],
153//! which measures the CHARGE rather than the count.
154//!
155//! # It is tighter than "N stores at once", because the kernel reclaims lazily
156//!
157//! `io_uring` teardown runs off a workqueue after the ring's last descriptor
158//! closes, so the pages of a **dropped** writer stay charged for a while. Opening
159//! and dropping one at a time and holding *nothing*, the 64 KiB writer was
160//! refused at the **63rd**. A server that opens a store per repository and lets
161//! it go — gunnar's `store_cache` map holds `Weak`s, so that is its shape while
162//! seeding — therefore hits this after about sixty repositories, which is where
163//! the sweep's `vs_forge_*` arms died seeding.
164//!
165//! The accounting is on the `user_struct`, so it also crosses process
166//! boundaries: a fresh process was refused its **second** ring while a previous
167//! test process's rings were still being torn down. That is also why there is no
168//! guard on the serial shape — one written against it failed at 54 of 160 purely
169//! on the residue of the guard that ran before it, and a test whose verdict
170//! depends on what else the box did in the last few seconds is not a guard. The
171//! serial ceiling is bounded by the same per-store cost the guards below do
172//! measure, and it clears with pacing: 600 rings created and dropped 5 ms apart
173//! did not fail once.
174//!
175//! # If the kernel cannot do it
176//!
177//! [`UringWriter::create`] probes for `IORING_OP_WRITE`, `IORING_OP_WRITE_FIXED`
178//! and `IORING_OP_FSYNC` and fails with a named error if any is missing, rather
179//! than silently degrading to `pwrite` and reporting an io_uring number that is
180//! not one. Same for `io_uring_setup` being blocked outright
181//! (`kernel.io_uring_disabled=2`, seccomp, a container without the syscall).
182
183#![cfg(target_os = "linux")]
184
185use std::fs::File;
186use std::os::unix::fs::FileExt;
187use std::os::unix::io::AsRawFd;
188use std::path::{Path, PathBuf};
189use std::sync::Mutex;
190use std::sync::atomic::{AtomicU64, Ordering};
191
192use anyhow::{Result, anyhow, bail};
193use io_uring::{IoUring, opcode, squeue, types};
194
195use crate::archive_write::{
196    ArchiveWrite, Extent, encode_journal_row, encode_journal_schema, journal_path, open_blobs,
197};
198
199/// Size of the registered staging buffer the journal message is built in.
200///
201/// **One page, because a registration is pinned memory charged uid-wide against
202/// `RLIMIT_MEMLOCK`** — see the module docs for the arithmetic and the measured
203/// failure. A page is the smallest thing the kernel can pin, so this is the
204/// floor, and it is not tight: an encoded journal row is two `u64`s plus Arrow
205/// IPC framing — **224 bytes**, measured by `encode_journal_row` on oden
206/// 2026-08-14 — so one page is still 18× headroom. A row that ever outgrew it
207/// would be refused by name in [`UringWriter::append`] rather than silently
208/// truncated.
209///
210/// It was 64 KiB until 2026-08-14. That is 16 pages per store on top of the
211/// ring's own ~5, and it is what capped a process at ~87 io_uring stores.
212///
213/// **The size is only half of it.** What the kernel charges depends on where the
214/// page came from, not only on how many there are — see [`Staging`] and the
215/// module docs' second cost section. One page out of mimalloc's huge-page arena
216/// is charged 512.
217const JOURNAL_STAGING: usize = 4096;
218
219/// Largest single `Write` SQE. A blob bigger than this is split across several
220/// linked `Write`s in the *same* chain, so the ordering guarantee is unchanged.
221const MAX_WRITE_CHUNK: usize = 1 << 30; // 1 GiB
222
223/// Ring depth. Enough for 61 blob chunks (61 GiB) plus fsync/journal/fsync.
224const RING_ENTRIES: u32 = 64;
225
226/// This kernel's page size — the granularity a registration is pinned at, so
227/// the unit [`JOURNAL_STAGING`] is really measured in.
228pub(crate) fn page_size() -> usize {
229    // SAFETY: `sysconf` takes no pointers and cannot fail destructively; a
230    // negative return (it has none for `_SC_PAGESIZE` on Linux) falls back to
231    // the architectural 4 KiB.
232    let n = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
233    if n > 0 { n as usize } else { 4096 }
234}
235
236/// The soft `RLIMIT_MEMLOCK` in bytes, or `None` if it is unlimited or
237/// unreadable.
238///
239/// This is the ceiling every `IORING_REGISTER_BUFFERS` in the process is
240/// charged against, and the kernel keeps the running total on the
241/// **`user_struct`** — so it is shared with every other process this uid is
242/// running. That is why an io_uring store can be refused by a ring some
243/// unrelated process of the same user opened.
244pub(crate) fn memlock_limit_bytes() -> Option<u64> {
245    let mut lim = libc::rlimit {
246        rlim_cur: 0,
247        rlim_max: 0,
248    };
249    // SAFETY: `lim` is a live, correctly-typed `rlimit` this call only writes.
250    if unsafe { libc::getrlimit(libc::RLIMIT_MEMLOCK, &mut lim) } != 0 {
251        return None;
252    }
253    if lim.rlim_cur == libc::RLIM_INFINITY {
254        None
255    } else {
256        Some(lim.rlim_cur as u64)
257    }
258}
259
260/// The registered staging buffer: **its own anonymous mapping, one page,
261/// `MADV_NOHUGEPAGE`** — never the process allocator's memory.
262///
263/// It is a mapping rather than a `Box<[u8]>` for one reason, and it is the
264/// module docs' second "what a registration costs" section: the kernel charges
265/// a registration by `compound_head`, so a page handed out by an allocator that
266/// `madvise(MADV_HUGEPAGE)`s its arenas — mimalloc, which `gunnar serve` runs on
267/// — is charged as **512 pages**. Its own mapping cannot be part of anybody's
268/// huge page, and `MADV_NOHUGEPAGE` says so to `khugepaged` as well rather than
269/// relying on a one-page VMA being too small to collapse.
270///
271/// The address is stable for the mapping's whole life, which is what the
272/// registration requires, and it is unmapped in [`Drop`] **after** the ring that
273/// registered it has been dropped (field order in [`Ring`]).
274struct Staging {
275    ptr: *mut u8,
276    len: usize,
277}
278
279impl Staging {
280    /// Map `len` bytes, rounded up to whole pages, refusing huge pages.
281    fn map(len: usize) -> Result<Self> {
282        let page = page_size();
283        let len = len.next_multiple_of(page).max(page);
284        // SAFETY: a fresh anonymous mapping; no pointer of ours is passed in.
285        let ptr = unsafe {
286            libc::mmap(
287                std::ptr::null_mut(),
288                len,
289                libc::PROT_READ | libc::PROT_WRITE,
290                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
291                -1,
292                0,
293            )
294        };
295        if ptr == libc::MAP_FAILED {
296            return Err(anyhow!(
297                "uring: mmap {len} B for the registered staging buffer: {}",
298                std::io::Error::last_os_error()
299            ));
300        }
301        // SAFETY: `ptr`/`len` are the mapping just returned.
302        let advised = unsafe { libc::madvise(ptr, len, libc::MADV_NOHUGEPAGE) };
303        if advised != 0 {
304            // ENOSYS/EINVAL means this kernel has no transparent huge pages at
305            // all, which is the state the advice was asking for. Anything else
306            // is a refusal to give it, and the registration would then be
307            // charged 512 pages instead of one — a silent 512× ceiling is
308            // exactly what this file was fixed for, so it is named, not
309            // swallowed.
310            let e = std::io::Error::last_os_error();
311            let benign = matches!(
312                e.raw_os_error(),
313                Some(libc::EINVAL) | Some(libc::ENOSYS)
314            );
315            if !benign {
316                // SAFETY: unmapping the mapping made three statements ago.
317                unsafe { libc::munmap(ptr, len) };
318                return Err(anyhow!(
319                    "uring: madvise(MADV_NOHUGEPAGE) on the staging buffer: {e}. Without it a \
320                     one-page registration can be charged as the whole 2 MiB huge page it sits \
321                     in, which caps a process at four io_uring stores on a stock 8 MiB \
322                     RLIMIT_MEMLOCK."
323                ));
324            }
325        }
326        Ok(Self {
327            ptr: ptr as *mut u8,
328            len,
329        })
330    }
331
332    fn len(&self) -> usize {
333        self.len
334    }
335    fn as_ptr(&self) -> *const u8 {
336        self.ptr
337    }
338    fn as_mut_slice(&mut self) -> &mut [u8] {
339        // SAFETY: `ptr..ptr+len` is our own live, readable, writable mapping,
340        // and `&mut self` is the only handle to it.
341        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
342    }
343}
344
345impl Drop for Staging {
346    fn drop(&mut self) {
347        // SAFETY: the mapping this struct owns, unmapped exactly once. The ring
348        // that registered it is dropped first (field order in `Ring`), so no
349        // registration references these pages any more.
350        unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) };
351    }
352}
353
354struct Ring {
355    ring: IoUring,
356    /// Registered index 0. Its own mapping, so its address is stable for the
357    /// lifetime of the registration even though `UringWriter` may move — and so
358    /// the kernel charges it one page. See [`Staging`].
359    staging: Staging,
360    journal_cursor: u64,
361}
362
363// SAFETY: every access to `Ring` goes through `UringWriter`'s `Mutex`, so the
364// submission and completion queues are never touched from two threads at once.
365// The registered buffer is owned here and outlives the registration (the ring is
366// dropped first, in field order).
367unsafe impl Send for Ring {}
368
369/// io_uring writer: write → fsync → journal → fsync as one linked submission.
370///
371/// # Durability on return
372///
373/// Identical to [`SafeWriter`](crate::archive_write::SafeWriter): the pack bytes
374/// are on the device and a journal row on the device references them. What
375/// differs is the cost of getting there — one `io_uring_enter` instead of four
376/// blocking syscalls — not what is promised.
377pub struct UringWriter {
378    blobs: File,
379    journal: File,
380    journal_file_path: PathBuf,
381    cursor: AtomicU64,
382    ring: Mutex<Ring>,
383}
384
385impl UringWriter {
386    /// Open `archive`, set up the ring, register the journal staging buffer, and
387    /// open the journal beside it — **appending** to the rows already there, and
388    /// emitting the Arrow IPC schema header only if the file is new.
389    ///
390    /// Returns a named error — never a silent fallback — if this kernel cannot
391    /// provide what the chain needs. Nothing on disk is touched before that
392    /// point is passed.
393    pub fn create(archive: &Path) -> Result<Self> {
394        let (blobs, end) = open_blobs(archive)?;
395        let jpath = journal_path(archive);
396
397        // **The ring first, the journal second.** Nothing here may touch the
398        // journal until this writer is certain it can be built: `create` can
399        // still fail on `register_buffers` (the uid-wide `RLIMIT_MEMLOCK`
400        // ceiling in the module docs), and a failed open that had already
401        // opened the journal would leave a store's durable ack log standing
402        // behind a writer that does not exist.
403        let ring = IoUring::new(RING_ENTRIES).map_err(|e| {
404            anyhow!(
405                "uring: io_uring_setup failed ({e}). This kernel cannot run the io_uring arm \
406                 (check /proc/sys/kernel/io_uring_disabled, seccomp, container policy). \
407                 No fallback is substituted — a pwrite number reported as an io_uring number \
408                 would be a lie."
409            )
410        })?;
411
412        let mut probe = io_uring::register::Probe::new();
413        ring.submitter()
414            .register_probe(&mut probe)
415            .map_err(|e| anyhow!("uring: register_probe: {e}"))?;
416        for (code, what) in [
417            (opcode::Write::CODE, "IORING_OP_WRITE"),
418            (opcode::WriteFixed::CODE, "IORING_OP_WRITE_FIXED"),
419            (opcode::Fsync::CODE, "IORING_OP_FSYNC"),
420        ] {
421            if !probe.is_supported(code) {
422                bail!(
423                    "uring: this kernel does not support {what}; the write→fsync→journal→fsync \
424                     chain cannot be built. Not degrading to pwrite."
425                );
426            }
427        }
428
429        let staging = Staging::map(JOURNAL_STAGING)?;
430        // SAFETY: `staging` owns its own mapping and is owned by the `Ring`
431        // below; it is neither moved nor unmapped until the ring is dropped,
432        // which happens before it (struct field order in `Ring`).
433        unsafe {
434            let iov = libc::iovec {
435                iov_base: staging.as_ptr() as *mut libc::c_void,
436                iov_len: staging.len(),
437            };
438            ring.submitter()
439                .register_buffers(std::slice::from_ref(&iov))
440                .map_err(|e| {
441                    anyhow!(
442                        "uring: register_buffers ({} B, {} page(s)): {e}. A registration is \
443                         pinned memory charged against RLIMIT_MEMLOCK, and the kernel keeps that \
444                         count on the user_struct — it is per-UID and shared with every other \
445                         process this user is running, not per-process. This one is currently \
446                         {}. Every io_uring store holds one registration for as long as it is \
447                         open, so N stores pin N pages; `fast` and `safe` register nothing and \
448                         have no such ceiling. The page(s) named here are what was ASKED for; \
449                         the kernel charges by compound_head, so a staging buffer that ended up \
450                         inside a transparent huge page would be charged the whole 2 MiB — see \
451                         `Staging`, which takes its own MADV_NOHUGEPAGE mapping so that cannot \
452                         happen.",
453                        JOURNAL_STAGING,
454                        JOURNAL_STAGING.div_ceil(page_size()),
455                        memlock_limit_bytes()
456                            .map(|b| format!("{b} B"))
457                            .unwrap_or_else(|| "unreadable".into()),
458                    )
459                })?;
460        }
461
462        // **The journal is a LOG and a reopen APPENDS to it** — the contract
463        // `archive_write`'s module docs state for every durable arm, and until
464        // 2026-08-14 this writer was the one that broke it: it opened with
465        // `File::create`, which truncates, so a store reopened on this arm
466        // erased the durable record that its packs had ever been acked, derived
467        // an empty crash-recovery diff, and restarted pack ordinals at 0 — see
468        // `indexer::packs_already_acked` for what a restarted ordinal does to a
469        // pack's objects. `truncate(false)` plus a cursor taken from the file's
470        // own length is what makes the two durable arms actually
471        // indistinguishable on disk, which is what LAW 5 already claimed of
472        // them.
473        let journal = std::fs::OpenOptions::new()
474            .read(true)
475            .write(true)
476            .create(true)
477            .truncate(false)
478            .open(&jpath)
479            .map_err(|e| anyhow!("uring: open journal {}: {e}", jpath.display()))?;
480        let already = journal
481            .metadata()
482            .map_err(|e| anyhow!("uring: stat journal {}: {e}", jpath.display()))?
483            .len();
484        // The schema message opens the stream and is written **once per file**,
485        // not once per writer: a second one mid-file makes arrow's
486        // `StreamReader` — and therefore `read_journal` — stop at it.
487        let journal_cursor = if already == 0 {
488            let schema = encode_journal_schema()?;
489            journal.write_all_at(&schema, 0)?;
490            journal.sync_all()?;
491            schema.len() as u64
492        } else {
493            already
494        };
495
496        Ok(Self {
497            blobs,
498            journal,
499            journal_file_path: jpath,
500            cursor: AtomicU64::new(end),
501            ring: Mutex::new(Ring {
502                ring,
503                staging,
504                journal_cursor,
505            }),
506        })
507    }
508
509    /// Path of the journal segment beside `archive`.
510    pub fn journal_path(archive: &Path) -> PathBuf {
511        journal_path(archive)
512    }
513
514    /// The blob file, for a reader that wants to `pread` an extent back.
515    pub fn blobs(&self) -> &File {
516        &self.blobs
517    }
518
519    /// The journal file path this writer is appending rows to.
520    pub fn journal_file(&self) -> &Path {
521        &self.journal_file_path
522    }
523}
524
525impl ArchiveWrite for UringWriter {
526    fn append(&self, bytes: &[u8]) -> Result<Extent> {
527        let len = bytes.len() as u64;
528        let offset = self.cursor.fetch_add(len, Ordering::SeqCst);
529
530        let chunks = bytes.len().div_ceil(MAX_WRITE_CHUNK).max(1);
531        if chunks + 3 > RING_ENTRIES as usize {
532            bail!(
533                "uring: {} B needs {chunks} linked writes, more than the ring's {RING_ENTRIES} \
534                 entries",
535                bytes.len()
536            );
537        }
538
539        let row = encode_journal_row(offset, len)?;
540        let mut g = self
541            .ring
542            .lock()
543            .map_err(|_| anyhow!("uring mutex poisoned"))?;
544        if row.len() > g.staging.len() {
545            bail!(
546                "uring: journal row is {} B, staging buffer is {} B",
547                row.len(),
548                g.staging.len()
549            );
550        }
551        g.staging.as_mut_slice()[..row.len()].copy_from_slice(&row);
552        let journal_at = g.journal_cursor;
553
554        let blob_fd = types::Fd(self.blobs.as_raw_fd());
555        let journal_fd = types::Fd(self.journal.as_raw_fd());
556        let staging_ptr = g.staging.as_ptr();
557
558        let mut sqes: Vec<squeue::Entry> = Vec::with_capacity(chunks + 3);
559        // 1..=chunks — the pack bytes, verbatim, straight from the caller's slice.
560        for c in 0..chunks {
561            let start = c * MAX_WRITE_CHUNK;
562            let n = (bytes.len() - start).min(MAX_WRITE_CHUNK);
563            sqes.push(
564                opcode::Write::new(blob_fd, unsafe { bytes.as_ptr().add(start) }, n as u32)
565                    .offset(offset + start as u64)
566                    .build()
567                    .flags(squeue::Flags::IO_LINK)
568                    .user_data(c as u64),
569            );
570        }
571        // chunks+1 — blob bytes durable BEFORE the row that references them.
572        sqes.push(
573            opcode::Fsync::new(blob_fd)
574                .build()
575                .flags(squeue::Flags::IO_LINK)
576                .user_data(0xF5_00),
577        );
578        // chunks+2 — the journal row, out of the registered buffer, no BufWriter.
579        sqes.push(
580            opcode::WriteFixed::new(journal_fd, staging_ptr, row.len() as u32, 0)
581                .offset(journal_at)
582                .build()
583                .flags(squeue::Flags::IO_LINK)
584                .user_data(0x30_01),
585        );
586        // chunks+3 — the row is durable. Chain end: no IO_LINK.
587        sqes.push(opcode::Fsync::new(journal_fd).build().user_data(0xF5_01));
588
589        let want = sqes.len();
590        // SAFETY: every buffer referenced by an SQE (`bytes`, `g.staging`)
591        // outlives the `submit_and_wait` below, which does not return until all
592        // `want` operations have completed. The fds outlive `self`.
593        unsafe {
594            g.ring
595                .submission()
596                .push_multiple(&sqes)
597                .map_err(|e| anyhow!("uring: submission queue full: {e}"))?;
598        }
599        g.ring
600            .submit_and_wait(want)
601            .map_err(|e| anyhow!("uring: io_uring_enter: {e}"))?;
602
603        let mut written = 0i64;
604        let mut seen = 0usize;
605        let mut journal_written = 0i64;
606        for cqe in g.ring.completion() {
607            seen += 1;
608            let res = cqe.result();
609            if res < 0 {
610                let e = std::io::Error::from_raw_os_error(-res);
611                bail!(
612                    "uring: op {:#x} failed: {e} (a linked chain cancels its tail with \
613                     ECANCELED, so no journal row was written after a failed blob fsync)",
614                    cqe.user_data()
615                );
616            }
617            match cqe.user_data() {
618                0xF5_00 | 0xF5_01 => {}
619                0x30_01 => journal_written = res as i64,
620                _ => written += res as i64,
621            }
622        }
623        if seen != want {
624            bail!("uring: expected {want} completions, saw {seen}");
625        }
626        if written as u64 != len {
627            bail!(
628                "uring: short write — asked for {len} B, kernel wrote {written} B (io_uring \
629                 Write is not write_all)"
630            );
631        }
632        if journal_written as usize != row.len() {
633            bail!(
634                "uring: short journal write — {} B of {} B",
635                journal_written,
636                row.len()
637            );
638        }
639        g.journal_cursor = journal_at + row.len() as u64;
640
641        Ok((offset, len))
642    }
643
644    fn name(&self) -> &'static str {
645        "UringWriter"
646    }
647
648    fn durability(&self) -> &'static str {
649        "full — kernel-ordered blob fsync then journal fsync, one io_uring_enter"
650    }
651}
652
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657    use crate::archive_write::{ArchiveWrite, read_journal};
658
659    fn loadavg() -> String {
660        std::fs::read_to_string("/proc/loadavg")
661            .unwrap_or_default()
662            .split_whitespace()
663            .take(3)
664            .collect::<Vec<_>>()
665            .join(" ")
666    }
667
668    // ── the instrument the charge guard is built on ─────────────────────────
669    //
670    // `user->locked_vm` is not readable from userspace, so the only honest way
671    // to ask what a registration COST is to ask the kernel what can still be
672    // registered afterwards. `probe` is one long-lived ring; `scratch` is one
673    // big NOHUGEPAGE mapping. Registering and *explicitly* unregistering
674    // releases the charge synchronously — unlike dropping a ring, whose
675    // un-accounting runs off a workqueue — so the measurement leaves no residue
676    // of its own.
677
678    /// **The two guards that measure `RLIMIT_MEMLOCK` may not run at once.**
679    ///
680    /// `cargo test` runs them on different threads of ONE process and the budget
681    /// is a single uid-wide counter, so they are not independent measurements of
682    /// anything: [`Headroom::pages`] transiently registers the whole limit to
683    /// find out what is left, which is indistinguishable — to
684    /// [`enough_uring_writers_for_a_population_coexist_in_one_process`] — from
685    /// the ceiling it exists to detect. Seen: that guard failing at writer 84 of
686    /// 160 purely because the charge guard was mid-binary-search beside it.
687    ///
688    /// This does not pretend to serialise other PROCESSES; the module docs
689    /// already say the budget is shared uid-wide and that no guard here can own
690    /// it. It removes the one source of interference that is ours.
691    static MEMLOCK_MEASUREMENT: Mutex<()> = Mutex::new(());
692
693    fn measuring_memlock() -> std::sync::MutexGuard<'static, ()> {
694        MEMLOCK_MEASUREMENT
695            .lock()
696            .unwrap_or_else(|p| p.into_inner())
697    }
698
699    /// Big enough to ask for the whole stock 8 MiB limit in one registration.
700    const HEADROOM_CAP_PAGES: usize = 2048;
701
702    struct Headroom {
703        probe: IoUring,
704        scratch: Staging,
705    }
706
707    impl Headroom {
708        fn new() -> Self {
709            Self {
710                probe: IoUring::new(8).expect("a probe ring"),
711                scratch: Staging::map(HEADROOM_CAP_PAGES * page_size())
712                    .expect("a scratch mapping"),
713            }
714        }
715        fn fits(&self, pages: usize) -> bool {
716            let iov = libc::iovec {
717                iov_base: self.scratch.as_ptr() as *mut libc::c_void,
718                iov_len: pages * page_size(),
719            };
720            // SAFETY: `iov` names our own live scratch mapping, and the
721            // registration is dropped again before this function returns.
722            let ok = unsafe {
723                self.probe
724                    .submitter()
725                    .register_buffers(std::slice::from_ref(&iov))
726            }
727            .is_ok();
728            if ok {
729                self.probe
730                    .submitter()
731                    .unregister_buffers()
732                    .expect("unregister the probe buffer");
733            }
734            ok
735        }
736        /// Largest registration, in pages, this uid could make right now.
737        fn pages(&self) -> usize {
738            if self.fits(HEADROOM_CAP_PAGES) {
739                return HEADROOM_CAP_PAGES;
740            }
741            let (mut lo, mut hi) = (0usize, HEADROOM_CAP_PAGES);
742            while lo + 1 < hi {
743                let mid = (lo + hi) / 2;
744                if self.fits(mid) { lo = mid } else { hi = mid }
745            }
746            lo
747        }
748    }
749
750    /// A 2 MiB-aligned arena the kernel really has backed with a transparent
751    /// huge page, or `None` if this box will not give one.
752    struct HugeArena {
753        raw: *mut libc::c_void,
754        raw_len: usize,
755        arena: *mut u8,
756    }
757
758    impl HugeArena {
759        const HUGE: usize = 2 << 20;
760        fn new() -> Option<Self> {
761            let raw_len = 2 * Self::HUGE;
762            // SAFETY: a fresh anonymous mapping.
763            let raw = unsafe {
764                libc::mmap(
765                    std::ptr::null_mut(),
766                    raw_len,
767                    libc::PROT_READ | libc::PROT_WRITE,
768                    libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
769                    -1,
770                    0,
771                )
772            };
773            if raw == libc::MAP_FAILED {
774                return None;
775            }
776            let arena = ((raw as usize + Self::HUGE - 1) & !(Self::HUGE - 1)) as *mut u8;
777            // SAFETY: `arena..arena+HUGE` is inside the mapping above.
778            unsafe {
779                if libc::madvise(arena as *mut libc::c_void, Self::HUGE, libc::MADV_HUGEPAGE) != 0 {
780                    libc::munmap(raw, raw_len);
781                    return None;
782                }
783                // Fault it in, which is when the huge page is actually formed.
784                std::ptr::write_bytes(arena, 1u8, Self::HUGE);
785            }
786            Some(Self {
787                raw,
788                raw_len,
789                arena,
790            })
791        }
792        /// A single page well inside the arena.
793        fn a_page(&self) -> *const u8 {
794            // SAFETY: four pages in, still inside the 2 MiB arena.
795            unsafe { self.arena.add(4 * page_size()) }
796        }
797    }
798
799    impl Drop for HugeArena {
800        fn drop(&mut self) {
801            // SAFETY: the mapping this struct owns.
802            unsafe { libc::munmap(self.raw, self.raw_len) };
803        }
804    }
805
806    /// **A registration is charged one page — not the 2 MiB huge page the page
807    /// happens to live in.**
808    ///
809    /// This is the guard for the defect that survived the first fix of this
810    /// file's ceiling and was found on 2026-08-14: `io_buffer_account_pin`
811    /// charges by `compound_head`, so one 4 KiB registration taken out of an
812    /// allocator arena that has been `madvise(MADV_HUGEPAGE)`d costs **512
813    /// pages**. `gunnar serve` runs on mimalloc, which does exactly that, and
814    /// the result was a server that could open **three** io_uring stores on a
815    /// stock 8 MiB `RLIMIT_MEMLOCK` — four huge pages, one of them the control
816    /// store — while [`enough_uring_writers_for_a_population_coexist_in_one_process`]
817    /// went green at 160 writers in a `cargo test` binary that is on the system
818    /// allocator and therefore never had the bug.
819    ///
820    /// **It measures the charge, not the count**, because the count is what the
821    /// blind guard already measures. The charge is read off the kernel — the
822    /// only place it exists — by binary-searching what can still be registered.
823    ///
824    /// **The positive control comes first and is not optional.** A box whose
825    /// transparent huge pages are off cannot express this defect at all, and a
826    /// guard that quietly passed there would be measuring nothing; so the huge
827    /// page is built by hand, its charge is measured, and it must be at least
828    /// 256 pages before the real assertion is believed.
829    ///
830    /// Noise: `RLIMIT_MEMLOCK` is charged uid-wide, so another process opening
831    /// or closing a ring between the two readings moves a difference. Five
832    /// readings are taken and the SMALLEST is asserted on — a false red would
833    /// need five consecutive intrusions, and the two outcomes are 1 and 514, so
834    /// there is no band where noise decides it.
835    ///
836    /// Seen RED by making `Staging::map` hand back a page out of a
837    /// `MADV_HUGEPAGE` arena instead of its own mapping — which is exactly what
838    /// `Box<[u8]>` did under mimalloc: *"writer 3 of 7 could not even be opened,
839    /// with 428 page(s) of RLIMIT_MEMLOCK headroom left and the earlier writers
840    /// charged [514, 512, 514] page(s) each. One page inside a transparent huge
841    /// page costs 514 on this box…"*. Restored. Note the shape of that red: the
842    /// bound below is not even reached, because four registrations is the whole
843    /// budget — which is the server's measured ceiling of three repositories
844    /// plus its control store, arrived at from the other end.
845    #[test]
846    fn a_registration_costs_one_page_and_not_the_huge_page_it_might_sit_in() {
847        let _serial = measuring_memlock();
848        let h = Headroom::new();
849
850        // ── positive control ────────────────────────────────────────────────
851        let Some(arena) = HugeArena::new() else {
852            panic!(
853                "this box would not give a transparent huge page (madvise refused), so it \
854                 cannot exhibit the 512× registration charge this guard exists for. That is a \
855                 property of the box, not a pass."
856            );
857        };
858        let mut huge_charge = 0usize;
859        for _ in 0..5 {
860            let before = h.pages();
861            let ctl = IoUring::new(RING_ENTRIES).expect("a control ring");
862            let iov = libc::iovec {
863                iov_base: arena.a_page() as *mut libc::c_void,
864                iov_len: page_size(),
865            };
866            // SAFETY: `iov` names one page of the live arena above; the
867            // registration is released before the arena is dropped.
868            unsafe {
869                ctl.submitter()
870                    .register_buffers(std::slice::from_ref(&iov))
871                    .expect("registering one page of a huge page")
872            };
873            let after = h.pages();
874            huge_charge = huge_charge.max(before.saturating_sub(after));
875            ctl.submitter()
876                .unregister_buffers()
877                .expect("release the control registration");
878        }
879        assert!(
880            huge_charge >= 256,
881            "the positive control charged only {huge_charge} page(s) for one page inside a \
882             MADV_HUGEPAGE arena, so transparent huge pages are not actually in play here and \
883             the assertion below would pass on any implementation"
884        );
885
886        // ── the writer itself ───────────────────────────────────────────────
887        let dir = crate::store::tests::tmpdir("uring-registration-charge");
888        let mut readings = Vec::new();
889        for i in 0..7 {
890            let before = h.pages();
891            let w = UringWriter::create(&dir.join(format!("objects.pack.{i}")))
892                .unwrap_or_else(|e| {
893                    panic!(
894                        "writer {i} of 7 could not even be opened, with {before} page(s) of \
895                         RLIMIT_MEMLOCK headroom left and the earlier writers charged \
896                         {readings:?} page(s) each. One page inside a transparent huge page \
897                         costs {huge_charge} on this box, so a staging buffer that is not its \
898                         own MADV_NOHUGEPAGE mapping caps a process at four registrations: \
899                         {e:#}"
900                    )
901                });
902            let after = h.pages();
903            // Applied output, not just an open: the chain runs through the
904            // registered buffer that was just measured.
905            let (o, l) = w.append(b"one pack down the io_uring chain").unwrap();
906            assert_eq!(
907                read_journal(w.journal_file()).unwrap(),
908                vec![(o, l)],
909                "writer {i} acked without a journal row out of the registered buffer"
910            );
911            readings.push(before.saturating_sub(after));
912            drop(w);
913        }
914        readings.sort_unstable();
915        let charge = readings[readings.len() / 2];
916        assert!(
917            charge <= 64,
918            "a UringWriter's registration was charged {charge} page(s) (readings {readings:?}). \
919             One page inside a transparent huge page costs {huge_charge} on this box, and that \
920             is what a `Box<[u8]>` buys you under an allocator that madvises its arenas — \
921             mimalloc, which `gunnar serve` runs on. The staging buffer must be its own \
922             MADV_NOHUGEPAGE mapping (`Staging::map`), which costs the ring plus exactly one \
923             page."
924        );
925        // …and it costs SOMETHING. A charge of zero would mean the registration
926        // this whole file is built on did not happen, and every bound above
927        // would be satisfied by a writer that registers nothing at all.
928        assert!(
929            charge >= 1,
930            "a UringWriter cost 0 page(s) of RLIMIT_MEMLOCK (readings {readings:?}), so nothing \
931             was registered and the bound above is vacuous"
932        );
933        eprintln!(
934            "load {}; a UringWriter costs {charge} page(s) of RLIMIT_MEMLOCK (readings \
935             {readings:?}); one page inside a huge page costs {huge_charge}",
936            loadavg(),
937        );
938    }
939
940    /// **How many writers this box's `RLIMIT_MEMLOCK` has to be able to carry**,
941    /// and the limit and page size it was derived from.
942    ///
943    /// Derived rather than hardcoded, so the two guards below state the same
944    /// thing on any box: `n × 16 pages > limit`, so the 64 KiB registration this
945    /// file carried until 2026-08-14 **cannot** pass by construction, and
946    /// `n × 1 page ≤ limit / 4`, so a one-page registration passes with 4× of
947    /// margin on the registration alone. The real margin is smaller and is
948    /// stated where it is used: the ring costs ~5 more pages that this
949    /// arithmetic cannot see, and the limit is charged uid-wide and shared with
950    /// every other process this user is running.
951    fn writers_the_limit_must_allow() -> (usize, usize, usize) {
952        let page = page_size();
953        let Some(limit) = memlock_limit_bytes().map(|b| b as usize) else {
954            panic!(
955                "RLIMIT_MEMLOCK is unlimited on this box, so it cannot exhibit the ceiling these \
956                 guards exist for. That is a property of the box, not a pass: run them somewhere \
957                 with the stock 8 MiB limit before believing the arm scales."
958            );
959        };
960        // 16 pages is what this file registered per writer until 2026-08-14.
961        let n = (limit / (16 * page) + 32).min(limit / (4 * page));
962        assert!(
963            n >= 8,
964            "RLIMIT_MEMLOCK is only {limit} B on this box — too small for a guard to separate a \
965             1-page registration from a 16-page one"
966        );
967        (n, limit, page)
968    }
969
970    /// **Enough io_uring stores to serve a population coexist in one process.**
971    ///
972    /// This is the guard for the failure the whole file's "what a registration
973    /// costs" section is about. A `UringWriter` pins its registered staging
974    /// buffer for as long as it is open, against a `RLIMIT_MEMLOCK` the kernel
975    /// counts **per uid**; a server holds one writer per repository, so the
976    /// number of repositories a process can serve on this arm is
977    /// `RLIMIT_MEMLOCK / pinned-per-writer`. At 64 KiB per writer and the stock
978    /// 8 MiB limit that is ~87, and gunnar's `multiuser_scaling` alone provisions
979    /// 102 users with a repository each.
980    ///
981    /// `n` comes from [`writers_the_limit_must_allow`] — derived from this box's
982    /// own limit rather than hardcoded.
983    ///
984    /// Every writer **appends** — the registration is not the assertion, the
985    /// chain running through it is, and the last writer's journal is read back
986    /// off disk to prove the row landed.
987    ///
988    /// Seen RED by restoring the old buffer size, `JOURNAL_STAGING = 64 * 1024`:
989    /// "the io_uring arm ran out of pinned memory at writer 107 of 160:
990    /// uring: register_buffers (65536 B, 16 page(s)): Cannot allocate memory
991    /// (os error 12). …". Restored.
992    ///
993    /// **What it cannot see — and this one was expensive.** It counts writers,
994    /// not what each of them is CHARGED, and the charge depends on which
995    /// allocator the executable is on: a page out of a `madvise(MADV_HUGEPAGE)`d
996    /// arena is charged 512. A `cargo test` binary is on the system allocator
997    /// and never has that problem, so this guard was green at 160 writers on
998    /// 2026-08-14 while `gunnar serve` — on mimalloc — could open **three**
999    /// stores. [`a_registration_costs_one_page_and_not_the_huge_page_it_might_sit_in`]
1000    /// is the guard for that, and it measures the charge instead of counting.
1001    ///
1002    /// **What it cannot see.** `RLIMIT_MEMLOCK` is shared uid-wide, so another
1003    /// process of the same user holding pinned io_uring buffers eats the same
1004    /// budget. `n` is 160 on a stock 8 MiB limit against a measured ~400 that
1005    /// fit, so the margin is ~2.5× rather than the 4× the page arithmetic alone
1006    /// suggests — the ring itself costs ~5 pages that the arithmetic does not
1007    /// see. A box with an unlimited memlock cannot express this bug at all and
1008    /// the guard says so out loud rather than passing quietly.
1009    #[test]
1010    fn enough_uring_writers_for_a_population_coexist_in_one_process() {
1011        let _serial = measuring_memlock();
1012        let (n, limit, page) = writers_the_limit_must_allow();
1013        let dir = crate::store::tests::tmpdir("uring-memlock-ceiling");
1014        let payload = b"one pack down the io_uring chain".to_vec();
1015        let mut held: Vec<UringWriter> = Vec::with_capacity(n);
1016        let mut extents = Vec::with_capacity(n);
1017        for i in 0..n {
1018            let w = match UringWriter::create(&dir.join(format!("objects.pack.{i}"))) {
1019                Ok(w) => w,
1020                Err(e) => panic!(
1021                    "the io_uring arm ran out of pinned memory at writer {i} of {n}: {e:#}\n\
1022                     RLIMIT_MEMLOCK here is {limit} B and one page is {page} B, so {n} writers \
1023                     need {} B pinned. A server holds one writer per repository; this is the \
1024                     ceiling on how many repositories the arm can serve.",
1025                    n * page
1026                ),
1027            };
1028            extents.push(
1029                w.append(&payload)
1030                    .unwrap_or_else(|e| panic!("writer {i} of {n} could not append: {e:#}")),
1031            );
1032            held.push(w);
1033        }
1034
1035        // Applied output, off disk, from the last writer standing: the pack
1036        // bytes verbatim at the extent it returned, and a journal row naming it.
1037        let last = held.last().unwrap();
1038        let (o, l) = *extents.last().unwrap();
1039        let mut back = vec![0u8; l as usize];
1040        last.blobs().read_exact_at(&mut back, o).unwrap();
1041        assert_eq!(back, payload, "writer {} did not store the bytes", n - 1);
1042        assert_eq!(
1043            read_journal(last.journal_file()).unwrap(),
1044            vec![(o, l)],
1045            "writer {} acked without a journal row naming the extent",
1046            n - 1
1047        );
1048        eprintln!(
1049            "load {}; {n} io_uring writers open at once, {} B pinned of a {limit} B \
1050             RLIMIT_MEMLOCK ({page} B/writer; the 64 KiB registration this replaced would have \
1051             needed {} B)",
1052            loadavg(),
1053            n * page,
1054            n * 16 * page,
1055        );
1056    }
1057
1058    /// **A reopened io_uring writer APPENDS to its journal. It does not truncate
1059    /// it.**
1060    ///
1061    /// The journal is the durable half of §13.12's `indexed` bit and the only
1062    /// record that a pack was ever acked. `archive_write`'s module docs state
1063    /// for every durable arm that "a writer opened over an archive that already
1064    /// has a journal appends — it writes no second schema and it truncates
1065    /// nothing", and this arm did not obey it: `File::create` truncates, so a
1066    /// reopened store lost every earlier extent, re-queued nothing on the
1067    /// crash-recovery diff, and restarted pack ordinals at 0 — which is how a
1068    /// fresh pack takes the ordinal of one already absorbed and has its objects
1069    /// dropped (`indexer::packs_already_acked`).
1070    ///
1071    /// Asserted on the file: three appends across **three** writers over one
1072    /// archive, then the journal read back off disk with all three extents in
1073    /// append order. A single reopen would be enough for the truncation; the
1074    /// third proves the second writer did not simply start a second stream that
1075    /// `read_journal` stops at.
1076    ///
1077    /// Seen RED by restoring `File::create(&jpath)` (and the schema written
1078    /// unconditionally at offset 0): "the reopened io_uring writer lost the
1079    /// journal rows written before it: left: [(1024, 1024)] right: [(0, 512),
1080    /// (512, 512), (1024, 1024)]". Restored.
1081    #[test]
1082    fn a_reopened_uring_writer_appends_to_its_journal_rather_than_truncating_it() {
1083        let dir = crate::store::tests::tmpdir("uring-journal-reopen");
1084        let blobs = dir.join("objects.pack");
1085        let mut want = Vec::new();
1086        for (i, len) in [512usize, 512, 1024].into_iter().enumerate() {
1087            let w = UringWriter::create(&blobs)
1088                .unwrap_or_else(|e| panic!("open {i} of the same archive: {e:#}"));
1089            want.push(w.append(&vec![b'a' + i as u8; len]).unwrap());
1090            // Dropped here — the next iteration is a genuine reopen, including
1091            // the `flock` `open_blobs` takes.
1092        }
1093        assert_eq!(
1094            read_journal(&UringWriter::journal_path(&blobs)).unwrap(),
1095            want,
1096            "the reopened io_uring writer lost the journal rows written before it"
1097        );
1098        // …and the blob cursor resumed from the file rather than from zero, so
1099        // the extents do not overlap.
1100        assert_eq!(want, vec![(0, 512), (512, 512), (1024, 1024)]);
1101        eprintln!(
1102            "load {}; three io_uring writers over one archive: journal {:?}",
1103            loadavg(),
1104            want
1105        );
1106    }
1107
1108    /// The encoded journal row really does fit the one page that is pinned for
1109    /// it — measured through the encoder both arms share, not assumed.
1110    ///
1111    /// It is asked of `(u64::MAX, u64::MAX)` rather than of a real extent: the
1112    /// row is fixed-width Arrow IPC, so the widest values are the honest
1113    /// question, and a guard that only ever encoded small offsets would be
1114    /// sitting on an identity value.
1115    ///
1116    /// Seen RED by `JOURNAL_STAGING = 128`: "a journal row is 224 B and the
1117    /// registered staging buffer is 128 B". Restored. That is the same refusal
1118    /// [`UringWriter::append`] raises at run time, which is why shrinking the
1119    /// registration is safe to do at all: a row that outgrew the page is named,
1120    /// never truncated.
1121    #[test]
1122    fn a_journal_row_fits_the_page_that_is_pinned_for_it() {
1123        let row = crate::archive_write::encode_journal_row(u64::MAX, u64::MAX).unwrap();
1124        assert!(
1125            row.len() <= JOURNAL_STAGING,
1126            "a journal row is {} B and the registered staging buffer is {JOURNAL_STAGING} B",
1127            row.len()
1128        );
1129        eprintln!(
1130            "load {}; journal row {} B into a {JOURNAL_STAGING} B registered buffer ({} page)",
1131            loadavg(),
1132            row.len(),
1133            JOURNAL_STAGING / page_size()
1134        );
1135    }
1136}
1137