Skip to content

The marker records which object the publish is placing - #1

Merged
MorningLightMountain713 merged 2 commits into
mainfrom
feat/marker-records-published-identity
Aug 13, 2026
Merged

The marker records which object the publish is placing#1
MorningLightMountain713 merged 2 commits into
mainfrom
feat/marker-records-published-identity

Conversation

@MorningLightMountain713

Copy link
Copy Markdown
Collaborator

Why

FluxOS's boot sweep finds an interrupted publish as three things: the displaced data under .flux-old-<id>, the marker naming where it belongs, and whatever now occupies the destination. It has to decide whether that last thing is the object this publish placed — in which case the displaced copy is rubbish — or something the app owner put at that path while the destination stood empty, in which case deleting the displaced copy destroys their only copy.

The marker gave it nothing to decide with, so it guessed: it followed whatever was at the destination and asked whether it resolved to anything. That is a question about the host's filesystem rather than about this operation, and it is answerable by the app owner — an absolute symlink written inside a container names the container's root, not the host's, so resolving one on the host is asking the wrong filesystem and the answer leaks whether a host path exists.

What changed

The marker is now two lines: the destination relative to the volume root, as before, and the inode and modification time of the object being published, read before anything moves.

Both survive rename, which is what makes them usable — ctime does not, since rename updates it, and a recorded ctime would mismatch the moment the publish succeeded. The inode number alone is not enough either: filesystems reuse them, so an entry created at the destination afterwards can carry the one recorded here. An mtime to the nanosecond does not collide by accident.

It does not have to resist being forged. The app owner can read this file through the file browser and can set an mtime, but a marker they match only makes the sweep delete the data it was holding for them.

A behaviour change worth naming

Reading the identity first means a staging path that is not there now fails before the destination is displaced rather than after, so there is nothing left for a sweep to put back. Strictly better, and it is what the old tests were relying on.

Coverage

Both suites reached the interrupted state by publishing a staging path that did not exist, which no longer produces it. A directory published over its own parent does, deterministically and without a crash: the first rename carries the staging path away inside the destination, so the second finds nothing at it. It is also a move a user can ask for.

test level proves
TestAnInterruptedPublishLeavesTheDataAndAMarkerThatPlacesIt unit + container the real interrupted state: data displaced, destination empty, marker at the volume root carrying the relative path and the identity
TestTheMarkerRecordsTheIdentityOfWhatIsBeingPublished unit the record names the object being placed, not the one displaced, and fails if they are swapped
TestAMissingStagingPathFailsBeforeAnythingMoves unit + container the new early failure leaves the destination intact and no artefacts

The identity assertions derive the inode and mtime independently rather than calling identity(), so the format is pinned rather than echoed back, and they read it from the displaced copy — which is what proves the identity survived the rename.

The container suite reads the identity it expects from inside a container. An inode number belongs to the filesystem that issued it, and a bind mount does not always carry it across unchanged: Docker Desktop synthesises its own on macOS. On a node it does not — verified against a live loop-mounted ext4 app volume through a real bind mount, where host and container both report inode 14 for the same file. That equality is what the FluxOS side depends on, and it is not testable from this repo.

Consumer

FluxOS pins this image by manifest digest and reads the marker on its boot sweep. The sweep changes to compare the identity instead of resolving what it finds land alongside the digest bump there.

A sweep that finds the destination occupied cannot tell what is sitting
there - the object this publish placed, or one the app owner put at that
path while the destination stood empty. It was left to guess from whether
what it found resolved to anything, which is a question about the host's
filesystem rather than about this operation.

The marker now carries the inode and modification time of the object being
published, read before anything moves. Both survive rename; ctime does not,
since rename updates it. The number alone is not enough either - a
filesystem reuses inodes, so an entry created at the destination afterwards
can carry the one recorded here - and an mtime to the nanosecond does not
collide by accident.

Reading the identity first means a staging path that is not there now fails
before the destination is displaced rather than after, leaving nothing for a
sweep to put back.

The two suites reached the interrupted state by publishing a staging path
that did not exist, which no longer produces it. A directory published over
its own parent does, deterministically and without a crash: the first rename
carries the staging path away inside the destination, so the second finds
nothing at it. It is also a move a user can ask for.

The container suite reads the identity it expects from inside a container.
An inode number belongs to the filesystem that issued it, and a bind mount
does not always carry it across unchanged - Docker Desktop synthesises its
own on macOS, where a node's Linux bind mount hands back what the host sees.
Running the image for a foreign architecture under emulation puts a platform
warning ahead of the value, so the parse saw a dozen fields where it wanted
two and the arm64 job failed on noise rather than on anything the image did.

The exit code is already read past the same noise by scanning from the end.
@MorningLightMountain713
MorningLightMountain713 merged commit a501ebc into main Aug 13, 2026
5 checks passed
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.

1 participant