Skip to content

dofs: link staged chunks during sync apply - #87

Merged
aron-cf merged 2 commits into
mainfrom
pi/dofs-bugfix/15ab04
Aug 7, 2026
Merged

dofs: link staged chunks during sync apply#87
aron-cf merged 2 commits into
mainfrom
pi/dofs-bugfix/15ab04

Conversation

@aron-cf

@aron-cf aron-cf commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

dofs: link staged chunks during sync apply

Pulling a file into the durable object used to hold that file in memory twice. applyChanges read every chunk back out of storage, joined them into one whole-file buffer, and handed the buffer to writeFile, which then split it into chunks again. Both copies are live at the same time, so a 64 MiB file costs about 128 MiB inside an isolate whose budget is 128 MiB. One large file in a sync was enough to run the isolate out of memory.

By the time apply runs, the chunks are already in content-addressed storage: the sender staged them through pushObjects, or the receiver staged them from fetchObjects. So apply now links the file's inode directly to those staged chunks, keeping the mode, modification time, and chunk list the change entry declares. Payload bytes never enter the isolate at all. Declared sizes are still checked against what is really staged, so an interrupted or truncated write is caught before anything gets linked, and a linked chunk is reachable from vfs_chunks, which is what keeps garbage collection off it.

Peak ArrayBuffer bytes during a 64 MiB apply, sampled on every database call:

before after
baseline 22.1 MiB 23.1 MiB
peak 128.7 MiB 23.1 MiB (no growth)

Linking a chunk list the receiver did not produce needs two guards that came free when the bytes went through writeFile. The first is layout: positional reads find the chunk covering an offset by dividing that offset by the chunk size, and treat a chunk's start as its index times the chunk size. A local writer satisfies that by construction, but a chunk list off the wire only satisfies it while both sides use the same window size, so linkStagedChunksSync now rejects a list whose interior chunks are short or whose chunks overflow a window. The second is the read-only mount check. Sync already reports entries under a read-only mount as skipped, so behavior over the wire is unchanged, but the guard belongs in the write primitive rather than in one of its callers. Both checks run before the existing entry at the path is removed, because a batch is a sequence of independent transactions and a late failure would otherwise delete a file and put nothing in its place.

To see the old behavior and the new one for yourself, run the package tests under both backends:

npm run build --workspace @cloudflare/dofs
npm test --workspace @cloudflare/dofs
npm run test:workers --workspace @cloudflare/dofs

Two of the new tests spy on the database handle and assert that no query reads chunk payloads, which fails against the old code. Three cover the layout guard: a ragged chunk list is rejected, an oversized chunk is rejected, and a file already at the path survives a rejected entry. One covers the read-only mount guard, and one proves garbage collection leaves a linked blob alone even when its staging timestamp is old enough to sweep. The memory numbers came from a throwaway probe; asserting on peak isolate memory is too brittle to keep in the suite.

One thing this change does not fix: nothing verifies that a staged blob's bytes really hash to the hash they arrived under. The receive loops trust the sender, as stageBlob documents. Apply used to launder that trust by re-hashing the joined buffer, which turned a mislabeled blob into a manifest mismatch and an endless resync; now the declared hash is taken at face value, and a mislabeled blob would leave both sides agreeing on manifests while holding different bytes. The sender is the durable object's own container over a trusted channel, so this is not urgent. If we want the check back it belongs in pushObjects and the fetchObjects receive loop, and it costs one hash pass over every synced byte.

The fix in the first commit comes from a patch by Caio Nogueira; the second commit adds the two guards and the ordering fix.

Caio-Nogueira and others added 2 commits August 7, 2026 15:13
Sync receivers already stage chunk payloads in content-addressed
storage before applying file entries. Reading them back and
concatenating a whole-file buffer makes peak isolate memory roughly
twice the file size.

Link each file's existing chunk references directly while preserving
metadata and validating declared sizes. This keeps payload bytes out
of the apply path and relies on vfs_chunks reachability to protect
linked blobs from collection.
linkStagedChunksSync writes chunk rows from a list it did not
produce, so the checks writeFile used to run on the way into the
sync apply path have to live in the helper itself.

Positional reads find the chunk covering an offset by dividing that
offset by CHUNK_SIZE, and take a chunk's start offset to be its
index times CHUNK_SIZE. A local writer chunks with chunksOf and
satisfies that by construction, but a chunk list that arrived over
the wire only satisfies it while both sides window at the same
size. Reject a list whose interior chunks are short or whose chunks
overflow a window, so a sender that windows differently fails loudly
instead of producing a file whose bytes read back from the wrong
offsets.

Also restore the read-only mount check. applyChanges gates read-only
mount roots itself and reports the skipped entries, so behavior over
sync is unchanged, but the exported helper is a write primitive and
the guard belongs at the data layer.

Run both checks in applyFileEntry before the existing entry at the
path is removed. A batch is a sequence of independent transactions,
so a throw after the removal would drop a file and put nothing in
its place.
@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

馃 Changeset detected

Latest commit: 91139bc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@cloudflare/dofs Patch
@cloudflare/computer Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Thanks for your interest in Cloudflare Computer.

This repository does not accept unsolicited pull requests. Please use one of the accepted contribution paths instead:

If a maintainer asked you to open this pull request, they can add the allow-pr label and reopen it.

@github-actions github-actions Bot closed this Aug 7, 2026
@aron-cf aron-cf added the allow-pr Allow a PR to remain open. label Aug 7, 2026
@aron-cf aron-cf reopened this Aug 7, 2026
@aron-cf
aron-cf merged commit 8758b51 into main Aug 7, 2026
12 of 19 checks passed
@aron-cf
aron-cf deleted the pi/dofs-bugfix/15ab04 branch August 7, 2026 15:28
@github-actions github-actions Bot mentioned this pull request Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

allow-pr Allow a PR to remain open.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants