Skip to content

Self update: releases side by side, and the daemon updates itself - #12

Open
IsuraManchanayake wants to merge 11 commits into
basecompute:rc-0.1.3from
IsuraManchanayake:upstream/self-update
Open

IsuraManchanayake wants to merge 11 commits into
basecompute:rc-0.1.3from
IsuraManchanayake:upstream/self-update

Conversation

@IsuraManchanayake

Copy link
Copy Markdown
Collaborator

Stacked on #10: its diff includes #10 until that merges; this PR's own commits are the last five. I will rebase it onto rc-0.1.3 once #10 lands.

What changed

Releases live under $SUPERFLUID_HOME/versions/<v>/ with a current link that the superfluid, superfluid-workerd and superfluid-noded commands on PATH go through; each version finds its workers beside its own executable, so a running server keeps its own workers after an update.

superfluid self update [--version <v>] [--dry-run]   # the newest release that fits this machine
superfluid self check                                 # is a newer release out?
superfluid self list                                  # the versions here, current and previous
superfluid self use <v>                               # go back to a version still here
superfluid self install --from <dir> [--prefix <dir>] # what the install script runs

self update reads the release listing, takes the newest release for the platform that declares the engine's ABI here (by an asset named superfluid-basert-abi-<N>.txt, else compat.json; with llama.cpp or MLX only, the newest), downloads the tarball and its .sha256 into a staging directory, checks and unpacks, runs the copy's own --version and runtimes --json, flips current with one rename, keeps the replaced version for self use, and prunes the rest, never a version with a keep file or one a running process started from. Without --version it only moves forward. One update, install or switch runs at a time. An installed version is never written to. A copy placed by the old install script is kept under versions/ by its first update; Homebrew, cargo and source builds are refused with the right command named.

$ superfluid self update
superfluid: 0.1.3 -> 0.1.4 (published 2026-11-02; no baseRT engine here)
superfluid: downloading superfluid-aarch64-apple-darwin.tar.gz (16.6 MB)
superfluid: superfluid 0.1.4 is current (/Users/me/.superfluid/versions/current)
superfluid: 0.1.3 stays installed: `superfluid self use 0.1.3` goes back to it
superfluid: a running server keeps the version it started with; starting it again uses the new one

After the server is up, superfluid serve reads the listing at most once a day, every hour while it stays up, and prints one line when a release newer than the running one fits here, held off three days after publication; --offline or SUPERFLUID_NO_UPDATE_CHECK=1 turns it off, and SUPERFLUID_RELEASES points at another listing.

The install script now ends in self install, placing a release the same way; the release workflow attaches the ABI marker asset and the compat.json sidecar. New page: docs/serving/updating.md.

Why

Today re-running the install script is the only update, it overwrites in place with no way back, and it swaps the workers under a running server. A host that installs superfluid for its engine needs to place a specific release beside whatever is there and run that version. The notify-only choice follows gh, Deno and Ollama's server: the daemon never downloads on its own.

How it was tested

  • cargo clippy --all-targets -- -D warnings, and -p superfluid --no-default-features: clean.
  • cargo test -p superfluid selfupdate::: 13 tests against fake releases on disk with the listing passed in: the listing and the fit rule, where an executable came from, update, flip, prune, keep and use, the probe's refusals, immutability of an installed version and a replacement proved first, a switch waiting for a held lock, the migration keeping the old copy and a running legacy server's workerd, and the check announcing once and asking again when due.
  • With the built binary in a scratch home: self install from the built tree, self list, self check and self update --dry-run against the real GitHub listing (forward-only: 0.1.2 is older than this build), an update from a local 0.2.0 release with download, probe, flip, links and the previous kept, self use back and --version forward without a download, the notice inside a real serve on the 0.3.0 engine, and the refusal for a source build.
  • Not run: the install script end to end against a release (it has no self install yet to hand off to), and the release workflow.

Reviewed in the fork as IsuraManchanayake#8.

  • cargo clippy --all-targets -- -D warnings
  • cargo test
  • Changes a wire protocol, the WAL format or the tick ABI (says so above, version bumped)

baseRT 0.3.1 defines BASERT_ABI_VERSION and exports baseRT_abi_version():
two libraries with the same number drive the same way, whatever their
version numbers say. The build reads the macro from the vendored header
(now 0.3.1's), the loader resolves the symbol when the library has it, and
the numbers decide: equal serves, unequal is refused by number. A library
that states none (0.3.0 and older), or a superfluid built against a header
without the macro, is judged by major.minor as before. The symbol and
configuration-struct checks stay. Library carries the number it found.
…s; superfluid compat; exit 4

A probe may state the contract number it checked the runtime against
(Probe.abi: baseRT's engine ABI), the worker's check report carries it, and
`superfluid runtimes --json` shows it as `abi` on a ready runtime. The
basert row also carries `requires`, the header version and ABI number this
build was built for, whether or not an engine was found, so a host can
compare numbers itself. `superfluid compat [--json]` prints the same for a
release.

`superfluid serve` exits 4 when the runtime for the model is not installed
or cannot run here (its engine library missing or of another ABI), where a
flag it got wrong exits 2, so a host can tell the two apart.

The catalog finds superfluid-workerd and the shipped adapters beside the
real executable, links resolved: a superfluid reached through a symlink
(a package manager's bin/, a host's shim) kept running the engine
in-process because the link's directory held neither.
…at.json

The page names the contract a host builds on: how to start superfluid
(the real path, --runtime, --basert-lib and BASERT_KERNEL_LIB, --offline,
one --sessions per server, SUPERFLUID_HOME), how to ask for the
compatibility verdict before serving, how to choose a release from the
compat.json each release now carries beside its tarball, and the exit
codes. The CLI reference, the runtimes page and the changelog follow.
…he unnumbered line; the engine install chooses by ABI number

The vendored header is baseRT 0.4.0's, so the fallback for a library that
states no ABI number can no longer be "the header's minor": that would
refuse 0.3.0, which carries ABI 1's functions and layouts without stating
it. The line that does so is named instead, 0.3.x for ABI 1, and a library
without a number serves only if it is of that line. No later ABI has such
libraries.

A library of 0.4.0 or later is asked for the model configuration by size
(baseRT_get_config_into), into a zeroed struct of these bindings' own size,
so the struct may grow or shrink between them without a change of ABI
number; an older library is read by value with the size check as before.
Every read of the configuration goes through config_of().

`superfluid runtime install basert` chooses by the same rule the loader
applies: a release's ABI number, read from the basert-engine-abi-<N>.txt
asset baseRT publishes beside its bundles, or a pin's, with the 0.3.x line
accepted without one; another engine installs only with --untested.
`superfluid compat` and the runtimes report name the unnumbered line too.
The pack step wrote dist/<name>.compat.json, but the artifact the release
job downloads carried only the tarball and its digest, so the sidecar the
docs promise beside the tarball never reached the release.
…s itself

Releases live under $SUPERFLUID_HOME/versions/<v> with a `current` link
that the commands on PATH go through. `superfluid self update` picks the
newest release that fits the engine here, downloads and checks it, proves
the copy starts, flips the link, keeps the version it replaced for
`self use` and removes older ones; a running server is never written to.
`serve` reads the listing once a day after it is up and prints one line
when a newer release is out. The install script now places a release the
same way, through `self install`, and a copy it placed before is migrated
by its first update. The release carries the engine ABI number in an
asset name, and compat.json is attached as documented.
… is made under the lock, the in-place copy is kept, and the check goes on

Six findings of the review:
- A version installed here is kept as it is; a copy of the same version
  placed again is used only when the one here no longer works, and only
  after the new copy is proved where it is, with the old set aside until
  the new one is in place.
- The install script never writes through a link into versions/: a
  release from before self install is placed by the installed superfluid
  when one can, else copied with any link removed first.
- An update takes the lock before reading current, on_disk or deciding
  forward-only, so an update queued behind another acts on what that
  one left; self use takes the same lock.
- The first update of an in-place install keeps that copy under versions/
  as the version it is, records it as previous, and leaves its workers
  while a server still runs from it.
- The daily check goes on while the server runs: once an hour the cache
  is looked at again and the listing read when a day has passed.
… running from the install script's copy keeps its workerd

Two findings of the re-review. A replacement that fails mid-copy leaves
nothing of itself at the version's place and brings the copy set aside
back, reporting both failures if that fails too. While a server runs from
the copy the install script placed, only the superfluid command moves:
its superfluid-workerd, superfluid-noded and workers stay, a migrating
note is kept, and the next update, switch or install finishes the move
once nothing runs from there. The install script's own path over an
earlier copy keeps that copy as the version to go back to, as an update
does, and follows the same rule.
clippy's zombie_processes lint on the running-server test: the spawned
copy of the test binary is killed and waited for by a guard, whichever
way the test ends.

This branch has not been deployed

No deployments
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