Skip to content

feat(nixos): delta prefetch for upgrades via pifinder-differ - #42

Open
mrosseel wants to merge 5 commits into
nixosfrom
delta-updates
Open

feat(nixos): delta prefetch for upgrades via pifinder-differ#42
mrosseel wants to merge 5 commits into
nixosfrom
delta-updates

Conversation

@mrosseel

Copy link
Copy Markdown
Owner

What

Device-side applier for the delta-update transport. Before nixos_upgrade lets nix download whole store paths, it asks the pifinder-differ server (running beside Attic on the cache host) for byte-level zstd patches against same-stem store paths the device already holds — across all retained generations — and imports whatever it can reconstruct. Every imported path is one nix no longer downloads.

Measured server-side on real CI build pairs: 255 MiB of changed store paths compress to 1.5 MiB of patches (0.6%).

How

  • delta_updates.py (new, stdlib-only like nixos_upgrade):
    • candidate bases = newest same-stem paths in /nix/store
    • POST /delta → 200 (patch ready) / 202 (computing, bounded retry) / 204 (full download)
    • reconstructs the target NAR with zstd --patch-from against nix-store --dump of the base (canonical on both ends)
    • verifies sha256 of the reconstructed NAR before import — nix-store --import does not check the NAR against the path name, so this is the line between a bad patch and a corrupt store
    • assembles the import stream locally (export wire format) from the response's references + deriver — validated byte-identical against real nix-store --export output for paths with and without references/deriver
  • nixos_upgrade.py: one prefetch call between estimate_download and run_build, plus a re-estimate so progress denominators match what nix actually fetches
  • services.nix: new pifinder.deltaUrl option (default "" = disabled), env wiring, zstd in the upgrade service path

Safety

  • Off by default. Empty deltaUrl short-circuits before any work.
  • Best-effort per path: server down, bad patch, disk full, corrupt base (nix-store --verify-path) — each falls back to the normal binary-cache download; prefetch_deltas can never raise into the upgrade.
  • Decode window capped at 2^28 (256 MiB) on the device regardless of what the server offers.
  • Free-space check (2× NAR + patch + slack) before applying each path.

Tests

23 new unit tests (pytest -m unit): stem/path parsing, candidate ordering, byte-exact import-stream framing, protocol states, apply guardrails, never-raises. Full test_nixos_upgrade.py suite still passes.

Not in this PR

The server is currently loopback-only on the cache host; exposing it (e.g. https://deltas.pifinder.eu via Caddy) is a separate infra change, after which enabling is a one-line pifinder.deltaUrl setting per device.

🤖 Generated with Claude Code

mrosseel and others added 5 commits August 22, 2026 16:18
Before nix downloads whole store paths, ask the delta server for
byte-level zstd patches against same-stem paths the device already
holds (any retained generation), reconstruct the target NAR with
zstd --patch-from, verify sha256 BEFORE import, and register via an
import stream assembled locally from the response's references and
deriver. Everything imported is a path nix no longer downloads; every
failure falls back to the binary cache per path.

Off by default: pifinder.deltaUrl = "" disables the whole path. The
window_log from the server is capped at 2^28 (256 MiB decode memory)
on the device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rate limiting is per update, not per IP: prefetch opens a session with
POST /update-start naming the target toplevel; the server sizes the
request budget from that closure's real size and the token rides an
x-update-session header on every /delta and blob request. NAT-friendly
(each device gets its own budget) and unavailable to abuse (unknown
toplevels are refused). No session -> prefetch disabled for the run,
normal downloads as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream renumbered nearby-ranking to ADR 0030 (our 0029 was
byte-identical — dropped in favour of upstream's numbering) and its
0029 is the fov-gate ADR. Our delta-updates ADR moves to 0031.
Conflicts: nearby.py/test_nearby.py taken from upstream (same fix,
upstream evolved in review); observations_db.py keeps the nixos shared
identity cache which supersedes upstream's batch resolve;
test_ui_modules.py takes upstream (superset: adds the details-serialise
test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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