A Bug That Only Showed Up When Someone Was Fast
Here is a sequence that looks correct line by line. The engine receives a file from its owner. It streams the bytes to the app that asked. When the last byte has gone out, it finishes the bookkeeping: closes the temporary blob, hashes it, writes the index row. The app got its file, the cache got its entry. Nothing is wrong.
Now add a caller that is quick. The moment its response ends, it asks for the same file again, or reads the health page to check the counters. For a few milliseconds, the app has the whole body and the cache does not yet have the entry. The second request finds nothing, and becomes a second miss: another full transfer of bytes that were sitting in a temporary file one function call away from being kept. The health page, read at the same instant, shows a miss count one lower than the truth.
It was intermittent, as races are: whether the second request lost depended on a few milliseconds of scheduling, so the same sequence was fine one time and a second miss the next. Nothing in a single response looked wrong, because nothing in a single response was wrong. The defect was in the order of two steps that each worked. Delta's house sign records the rule that came out of it, in two parts: count a cache outcome when it is decided, and land a small body in the cache before its last byte goes out.
Count When Decided
The first part is about the counters. The moment the engine knows this read is a miss (the owner sent a 200, it has a validator, it fits the cap, and the disk floor allows storing), it increments misses, before any byte of the body moves. Anyone who reads health after the response began sees a count that already includes it. Hits, revalidations and stale serves are counted the same way, at the instant they are chosen, inside touch(), before the blob is sent.
Commit Before the Last Byte, for Small Bodies
The second part changes the order of work for every body up to 4 MiB, which covers every thumbnail and nearly every small file an app reads: read the whole body from the owner first, write it into the blob, commit the blob and the index row, and only then send the headers and the body to the caller. By the time the caller can possibly know its read is over, the entry already exists. Its next request is a revalidation or a hit, never a second miss.
The same order brings a second guarantee for free. Because the body is read whole before a single header goes out, a connection that the owner cuts halfway through is discovered while the engine can still choose what to answer. If an older copy is cached, the caller gets it labeled stale; otherwise it gets the named cause (the peer client reports exactly how far the body got). It never receives a 200 with a short body.
Streaming for Large Bodies, and Why
Larger bodies, up to the 512 MiB per-entry cap, cannot be held whole in memory per request without making the daemon's memory a function of how many big files are in flight. They stream: each block is written to the caller and into the blob writer as it passes. The commit happens after the last block, so the race exists for them in principle, but large files are not what a quick caller re-reads in a loop, and the trade is bounded memory. The stream has its own checks. A body that grows past the per-entry cap (512 MiB by default) overflows the blob writer; it is still served through to the caller, but not kept. (Delta's own comment on that line says "larger than it said", but the writer counts against the cap, and http.client never hands over more than the declared length anyway.) A body that ends short of its declared length is not committed at all; the blob writer is aborted and its temporary file removed. A cache entry is only ever a complete body.
| Body | Order | If the owner cuts it short |
|---|---|---|
| ≤ 4 MiB | read whole → commit → answer | stale copy or named cause; no header was sent |
| 4 MiB to 512 MiB | stream to caller and blob → commit | caller's client sees the short body as an error; nothing kept |
| > 512 MiB, no validator, or below the floor | stream through (pass) | caller's client sees the short body as an error |
The General Shape
Whenever an operation has a visible end (a response finishing, a function returning, a message being acknowledged), any state that a follow-up action depends on must be in place before that end becomes visible. Bookkeeping after the signal is a race with whoever trusts the signal. The same reasoning shows up again in this quest wherever a record and a signal meet: a transfer's chunk is recorded only after its bytes are verified and on disk, and a write call is answered only after its owner has committed it.