Skip to content

Fold restic backups into genebean.services.restic, fix fleet-wide prune performance#699

Merged
genebean merged 3 commits into
mainfrom
feat/restic-prune-fix
Jul 20, 2026
Merged

Fold restic backups into genebean.services.restic, fix fleet-wide prune performance#699
genebean merged 3 commits into
mainfrom
feat/restic-prune-fix

Conversation

@genebean

Copy link
Copy Markdown
Owner

Summary

  • Triggering kiosk-gene-desk's restic backup manually (to verify PR Consolidate kiosk-entryway and kiosk-gene-desk config #698's kiosk-backups consolidation) revealed it running 37+ minutes on a Pi 4 actively serving the kiosk display - the backup itself succeeded in seconds, what ran long was prune, a whole-repository operation that every host sharing the repo was redundantly re-running on its own daily schedule
  • Splits forget (cheap, --host-scoped, safe everywhere) from prune (expensive, whole-repo) via raw systemd units, since NixOS's pruneOpts always bundles them - nixnuc becomes the sole host running prune, gated by a new enablePruneJob option enforced to be true on exactly one host via a new pre-commit check (scripts/check-restic-prune-singleton.sh)
  • Replaces the bare modules/shared/nixos/restic.nix import with a proper genebean.services.restic module (home owns options, nixos reads back - same shape as chromium-kiosk/tailscale/kiosk-backups)
  • Separate fix: the same dbus-broker reload bug already patched at the system level also affects the per-user-session instance - hit during nixnuc's real deploy, which also tripped up the automatic rollback the same way

Test plan

  • nix flake check --all-systems passes
  • --dry-activate on all four affected hosts (kiosk-entryway, kiosk-gene-desk, hetznix01, nixnuc) before any real deploy
  • Real deploy via deploy-rs to all four, confirmed via magic-rollback
  • Zero failed units (system + user scope) post-deploy on every host
  • Backup paths unchanged on kiosk-gene-desk after the module migration
  • Forget-only job completes in ~21s on kiosk-gene-desk (down from 37+ minutes)
  • restic-fleet-prune.service triggered manually on nixnuc and completes successfully

🤖 Generated with Claude Code

https://claude.ai/code/session_018iUoDbZzHLBGu7jD3pVGz2

genebean and others added 3 commits July 20, 2026 01:29
Same bug as the system-level fix already in place (upstream
systemd/dbus-broker#37515 - Type=notify-reload without the
RELOADING=1/READY=1 handshake), but for the separate per-user-session
dbus-broker instance. Hit during a real deploy to nixnuc: the
user-session reload hung, failed the whole activation, and the automatic
rollback then tripped over the identical hang on its way back to the
previous generation. NixOS's systemd.user.services.* mirrors
systemd.services.* for the user instance, so the same
reloadIfChanged/restartIfChanged override applies there too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iUoDbZzHLBGu7jD3pVGz2
…ces.restic

Triggering kiosk-gene-desk's restic backup manually to verify the
kiosk-backups consolidation (PR #698) revealed it running 37+ minutes on
a Pi 4 actively serving the kiosk display. The actual backup succeeded in
seconds - what ran long was prune, a whole-repository operation (restic
dedupes data into blobs shared across every host's snapshots, so it has
to scan all of them regardless of which host's forget triggered it) that
NixOS's services.restic.backups.<name>.pruneOpts always bundles into
`forget --prune`. Every host sharing the repo (nixnuc, hetznix01, both
kiosks) was redundantly re-running that same expensive scan on its own
daily schedule.

Fix: split forget (cheap, --host scoped, safe to run everywhere) from
prune (expensive, whole-repo) via raw systemd units bypassing the
module's bundling. nixnuc - the one always-on host - becomes the sole
place prune ever runs, gated by a new enablePruneJob option. A pre-commit
check (scripts/check-restic-prune-singleton.sh) enforces exactly one host
has it set, since running prune on zero hosts means space is never
reclaimed and running it on more than one wastes redundant work.

Replaces the bare modules/shared/nixos/restic.nix import with a proper
genebean.services.restic module (home owns options, nixos reads back -
same shape as chromium-kiosk/tailscale/kiosk-backups), since this is now
genuinely reusable, per-host-configurable infrastructure rather than a
one-size-fits-all shared import. kiosk-backups.nix's own pre-reinstall
prune job gets the same forget/prune split and now asserts its
dependency on genebean.services.restic.enable rather than failing
obscurely without it.

Verified via deploy-rs across all four affected hosts (kiosk-entryway,
kiosk-gene-desk, hetznix01, nixnuc): dry-activate, real deploy with
magic-rollback confirmation, zero failed units post-deploy, backup paths
unchanged on kiosk-gene-desk, and the forget-only job completing in ~21s
on kiosk-gene-desk (down from 37+ minutes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iUoDbZzHLBGu7jD3pVGz2
Caught live on nixnuc: restic-fleet-prune ran 5+ hours without finishing,
still stuck on "finding data that is still in use for 190 snapshots" -
the read-only scan phase. The unit had no HOME or RESTIC_CACHE_DIR set,
so restic logged "unable to locate cache directory: neither
$XDG_CACHE_HOME nor $HOME are defined" and "running prune without a
cache, this may be very slow!" and paid the full remote-index cost on
every operation instead of reusing a local cache.

The module-generated restic-backups-daily.service already maintains a
cache at /var/cache/restic-backups-daily via its own CacheDirectory.
Every raw unit (restic-forget-daily, restic-forget-pre-reinstall-backstop,
restic-fleet-prune, restic-forget-kiosk-pre-reinstall) now points at that
same cache via RESTIC_CACHE_DIR, and each also declares its own
CacheDirectory (same name) rather than relying on restic-backups-daily
having already run first - CacheDirectory is idempotent, so multiple
units sharing the same one is safe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iUoDbZzHLBGu7jD3pVGz2
@genebean
genebean merged commit afd5f07 into main Jul 20, 2026
2 checks passed
@genebean
genebean deleted the feat/restic-prune-fix branch July 20, 2026 11:59
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