Skip to content

Create missing sync directories - #111

Merged
aron-cf merged 4 commits into
mainfrom
bug-fix
Aug 19, 2026
Merged

Create missing sync directories#111
aron-cf merged 4 commits into
mainfrom
bug-fix

Conversation

@aron-cf

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

Copy link
Copy Markdown
Collaborator

Fixes #107.

Sync entries are ordered by revision. A directory can have a newer revision than a file inside it, so the file may arrive first even though its parent exists on the sender.

For example, the receiver could see this stream:

revision 4: file /workspace/newdir/file.txt
revision 8: directory /workspace/newdir
revision 9: directory /workspace

The old apply path tried to create file.txt immediately. Since /workspace/newdir was not present on the receiver yet, the operation failed with:

parent directory missing: /workspace/newdir/file.txt

Retrying did not help because the sender returned the same entries in the same order. A failed pull could stop container changes from reaching durable storage, while a failed push to a replacement container could prevent every later command from starting.

This change makes the receiver create missing parent directories before it applies a file or symbolic link. The same stream now works like this:

1. Create /workspace and /workspace/newdir as needed.
2. Apply /workspace/newdir/file.txt.
3. Apply the later directory entries and their settings.

If an ancestor already exists as the wrong kind of entry, the incoming child still tells us that the sender sees a directory there. For example:

receiver: /workspace is a file or symbolic link
incoming: /workspace/newdir/file.txt
result:   replace /workspace with a directory, then apply the child

Replacing a symbolic link removes the link without removing its target. Missing ancestors that are shared with a registered read-only mount can also be created. Entries that overlap the read-only mount are still reported as skipped instead of aborting the batch.

The behavior applies to both sync directions. Streaming pulls use applyChanges, while pushes use the synchronous applyChangesSync path. Both now recover from child-before-parent ordering without changing the cursor, database schema, or RPC format.

The regression tests cover files and symbolic links through both apply paths. They also cover local files and symbolic links blocking an incoming parent directory, preservation of a symbolic link target, and parent creation next to a read-only mount.

Run the package tests locally with:

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

Formatting, linting, type checking, and the workspace build were also run successfully. A patch changeset records the behavior change.

@changeset-bot

changeset-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ec640e

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

This PR includes changesets to release 4 packages
Name Type
@cloudflare/computer Patch
@cloudflare/dofs Patch
@cloudflare/computer-rpc Patch
@cloudflare/computerd 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

@pkg-pr-new

pkg-pr-new Bot commented Aug 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@111

commit: 8ec640e

@aron-cf
aron-cf marked this pull request as ready for review August 18, 2026 13:49
devin-ai-integration[bot]

This comment was marked as resolved.

A child entry can arrive before its parent directory when the parent
has a newer revision. Applying that stream used to fail repeatedly
with a missing-parent error.

Create absent parent directories before applying files and symbolic
links. Replace conflicting ancestor files and symbolic links when the
incoming child requires a directory, and allow safe parent creation
next to read-only mounts. Cover the streaming pull path and synchronous
push path with regression tests.
devin-ai-integration[bot]

This comment was marked as resolved.

Protect read-only mount paths when replacing a blocking ancestor, and repair wrong-type ancestors for directory entries as well as files and symlinks.

Resolve the common existing-parent case with one query so deeply nested sync entries do not repeatedly walk each path prefix.
devin-ai-integration[bot]

This comment was marked as resolved.

Let file applies use an existing parent that resolves through a symlink. Keep structural parent repair for missing targets so child-first streams still converge.
Resolve a reachable symlinked parent to its real directory before applying files, directories, or symlinks. Keep replacing dangling symlink ancestors so child-first streams can still create their required parent directories.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +238 to +251
for (let i = 0; i < parts.length - 1; i++) {
const ancestorPath = `/${parts.slice(0, i + 1).join("/")}`;
const ancestor = resolveInode(db, ancestorPath, { followSymlinks: false });
if (ancestor === null) {
mkdirForSyncParents(db, parentPath, { recursive: true }, () => mtime);
return { path: canonical };
}
if (ancestor.type === "dir") continue;
const blockingRoot = readOnlyRootFor(db, ancestorPath);
if (blockingRoot !== undefined) return { path: canonical, blockingRoot };
removeInodeTreeAtPath(db, ancestorPath, ancestor.inode, ancestor.type);
mkdirForSyncParents(db, parentPath, { recursive: true }, () => mtime);
return { path: canonical };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 A shortcut link to a folder is deleted when the folder it points to lacks the needed subfolder

An existing shortcut that points at a real folder is deleted and replaced by a new empty folder (removeInodeTreeAtPath at packages/dofs/src/sync/apply.ts:248) whenever the required subfolder underneath it is missing, so everything that used to be visible through that shortcut disappears from that location.
Impact: Files a user could previously reach through a linked folder path silently vanish from that path after a sync, even though the same link is deliberately preserved when the subfolder happens to already exist.

Inconsistency between the reachable-parent fast path and the ancestor-repair loop in ensureParentDirectories

ensureParentDirectories (packages/dofs/src/sync/apply.ts:214-253) has two mutually inconsistent outcomes for the same symlink ancestor:

  1. Reachable case — /workspace -> /target (dir) and /target/newdir exists. resolveInodeWithoutSymlinks returns null (symlink on path), resolveInode(parentPath) follows to /target/newdir (dir), so the reachable branch at packages/dofs/src/sync/apply.ts:226-236 rewrites the entry path to /target/newdir/file.txt and preserves the symlink. This is the behaviour blessed by the new tests writes a file through a reachable symlink parent and creates a directory through a reachable symlink parent.

  2. Unreachable case — same /workspace -> /target (dir, contents intact) but /target/newdir does not exist. resolveInode("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/workspace/newdir") now returns null, so control drops into the loop at line 238. At i = 0, ancestorPath = "/workspace" resolves (with followSymlinks: false) to a symlink, readOnlyRootFor("/workspace") is undefined, and line 248 unlinks the symlink outright. mkdirForSyncParents then materialises a literal empty /workspace and /workspace/newdir.

After case 2 the contents of /target (e.g. keep.txt) are no longer reachable at /workspace/... at all, even though the only thing actually missing was one subdirectory under the link target. The minimal repair would have been to create newdir under the resolved target — exactly what case 1 already assumes is the right model.

The existing tests only exercise the symlink-destruction path with a dangling link (symlink(db, "/missing", "/workspace", ...) at packages/dofs/src/sync/apply.test.ts:260), so the resolvable-link-plus-missing-subdir combination is uncovered.

Prompt for agents
In packages/dofs/src/sync/apply.ts, ensureParentDirectories handles a symlink ancestor in two contradictory ways.

When the parent path resolves through a symlink to an existing directory (the branch around line 226 that calls resolveInode(parentPath) and pathOf), the symlink is preserved and the write is redirected to the resolved real path. The new tests assert this is the desired behaviour.

When the same symlink points at a real directory but the required subdirectory under it does not yet exist, resolveInode(parentPath) returns null, so control falls into the per-ancestor loop. There the symlink is treated as a 'blocking' ancestor and is removed via removeInodeTreeAtPath, then a literal directory chain is created in its place. The link target's contents are still on disk but are no longer visible under the link's path.

The two branches should agree. A reasonable approach is: before treating a symlink ancestor as blocking, follow it with resolveInode (followSymlinks: true). If it resolves to a directory, translate the remaining path components onto the resolved real path (via pathOf) and create the missing subdirectories there, preserving the link — mirroring what the reachable-parent branch already does. Only treat a symlink ancestor as blocking when it is dangling or resolves to a non-directory.

Add a regression test covering: /target exists as a directory with content, /workspace is a symlink to /target, /target/newdir does NOT exist, and a file entry for /workspace/newdir/file.txt is applied. The symlink and /target's existing content should survive.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@aron-cf
aron-cf merged commit 432fc47 into main Aug 19, 2026
19 checks passed
@aron-cf
aron-cf deleted the bug-fix branch August 19, 2026 12:46
@github-actions github-actions Bot mentioned this pull request Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sync wedges with "parent directory missing" when directory entries stream after their children (pull and push sides)

1 participant