Skip to main content

CHUNK_BUDGET

Constant CHUNK_BUDGET 

Source
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:

PathBoundWhy it cannot be chunked
Database::write_bulk_atomicnone — caller-sized VecD-014: the batch is one act under one stamp. Splitting it is the thing the method exists not to do
Database::archivemeasured 26.8 ms for 2,000 archivable edges; see Database::archive_windowedD-012: copy-then-delete must be atomic, or a crash between the phases duplicates or loses rows
rebuild_currentmeasured 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.