pub const CHUNK_BUDGET: Duration;Expand description
The latency bound chunk_rows is derived from (§5.1.5, D-058).
This is the golden rule’s actual content. §9 has carried it as a row count with a duration attached — “chunk commit, 500 rows ≤ 3 ms” — which reads as two requirements and is one: the duration is the requirement, and the row count is whatever satisfies it on a given path and machine.
3 ms is §9’s number, kept rather than renegotiated. What it buys, end to end:
an interactive assertion arriving at the worst possible moment waits for the
chunk in flight (≤ 3 ms, because the SQLite write lock is not preemptible —
see HighPriCommand) and then runs its own write (≤ 5 ms, §9), so ≤ 8 ms
worst case. That fits inside a 60 Hz frame with room, which is the standard
this bound is ultimately answerable to.
§Three operations are exempt, and the exemption is a contract, not an oversight
This was recorded in three separate rustdoc notes and nowhere near the bound itself, which is where a reader looks for its scope (§8.6). Stated here, with Wave 3’s measurements:
| Path | Bound | Why it cannot be chunked |
|---|---|---|
Database::write_bulk_atomic | none — caller-sized Vec | D-014: the batch is one act under one stamp. Splitting it is the thing the method exists not to do |
Database::archive | measured 26.8 ms for 2,000 archivable edges; see Database::archive_windowed | D-012: copy-then-delete must be atomic, or a crash between the phases duplicates or loses rows |
rebuild_current | measured 24.6 / 104 / 318 ms at 4K / 16K / 40K rows in links (was “~50 s per 10M edges”, which nothing had measured) | D-023: the window between DELETE and INSERT is the whole of current belief; a reader landing in it sees a graph with no edges and no error |
The archive figure is end-to-end through this method, so it includes
the re-derivation archive() runs inside its transaction — but it does not
attribute it, and until D-077 more than half of that re-derivation was an
audit comparing links_current against the query that had just filled it.
Note also which variable that cost scales with: rebuild_within reprojects
all of links, so the archive’s repair term grows with the surviving
table and not with the batch being archived. A budget stated per “100K closed
intervals” (§9) is
therefore parameterised on the wrong quantity.
All three are atomic by contract, which is why “cap the batch” and “add a third tier” were both considered and neither was taken: capping breaks the guarantee the operation exists to provide, and a third tier changes which caller waits without changing how long the lock is held. What was wrong was never the exemption — it was that the bound was stated as though it had none.
A caller who needs the latency bound and not the atomicity has
Database::bulk_import, which is the same write chunked at
chunk_rows::EDGES and explicitly not atomic overall (D-011).
§One of the three is no longer unbounded (T1.1, D-080)
archive was the worst of them, because its hold is a function of how long
since the last archive rather than of anything the caller chose.
Database::archive_windowed runs the same work as N sessions, each
atomic, each its own actor turn. Measured on an 8,000-key fixture with four
generations of superseded history: the longest single hold falls from
3.3 s to 0.77 s at one-hour windows, for total wall time that is flat
within this cycle’s noise.
The same measurement at 2,000 keys goes the other way — the hold falls
260 ms → 117 ms while total time rises 260 ms → 671 ms — so windowing is a
trade and not a free improvement. It pays when the backlog is large, which
is when the unwindowed hold is a problem in the first place. archive is
kept, not deprecated, for exactly that reason.