Disk→backend state migration with refuse-on-conflict #154

Closed
opened 2026-08-09 03:16:55 +00:00 by Cordy · 1 comment
Owner

Depends on #153. For existing s3-driver instances (the dogfood included) whose state lives at /data/.cairn/ today.

On boot, per state name:

  • local present + backend absent → upload once, then prefer backend. Local copy is renamed to <name>.migrated (kept as a fallback for one release cycle, ignored by the store).
  • backend present + local absent → backend wins, nothing to do.
  • both present and differing → refuse to start, log both checksums and mtimes, and tell the operator exactly which file to delete. Newest-wins is explicitly rejected: for holds.json an automatic pick could silently release a legal hold, and a refusing boot is a support ticket while a wrong merge is an incident.
  • both present and identical → adopt backend, rename local.

Migration runs before anything consumes state (slot between statestore construction and the first Load in the #153 boot order). Idempotent by construction — a crash mid-migration re-runs safely because upload-then-rename is per name.

Acceptance: dogfood upgrade path — deploy on the existing enc instance and verify its license, settings, shares, holds, peering and watermarks all appear under .cairn-state/ in the Garage bucket (ciphertext), the pod restarts clean, and the .migrated files sit untouched on the PVC.

Depends on #153. For existing s3-driver instances (the dogfood included) whose state lives at `/data/.cairn/` today. **On boot, per state name:** - local present + backend absent → upload once, then prefer backend. Local copy is renamed to `<name>.migrated` (kept as a fallback for one release cycle, ignored by the store). - backend present + local absent → backend wins, nothing to do. - **both present and differing → refuse to start**, log both checksums and mtimes, and tell the operator exactly which file to delete. Newest-wins is explicitly rejected: for `holds.json` an automatic pick could silently release a legal hold, and a refusing boot is a support ticket while a wrong merge is an incident. - both present and identical → adopt backend, rename local. Migration runs before anything consumes state (slot between statestore construction and the first Load in the #153 boot order). Idempotent by construction — a crash mid-migration re-runs safely because upload-then-rename is per name. Acceptance: dogfood upgrade path — deploy on the existing enc instance and verify its license, settings, shares, holds, peering and watermarks all appear under `.cairn-state/` in the Garage bucket (ciphertext), the pod restarts clean, and the `.migrated` files sit untouched on the PVC.
Author
Owner

Shipped in v0.5.2–v0.5.4 with #153 and executed live on the dogfood 2026-08-09. statestore.MigrateFile implements the policy exactly as specced: local-only → upload + retire to .migrated; backend-only/fresh → no-op; identical (byte compare) → retire; differing → refuse to start with both SHA-256 digests and the exact operator instruction (rename local to prefer backend, delete object to prefer local). Newest-wins rejected as designed — holds. Idempotent per name; runs in the bind loop before anything consumes state. All four cases unit-tested.

The acceptance run happened for real: the dogfood's live state — license, settings, shares, holds, peering trust, watermarks — migrated into the Garage bucket on the v0.5.4 boot. Evidence: instance peering … enabled=true in the boot log (that flag only exists in the migrated settings.json, read back from the bucket and decrypted). Two crashlooped deploy attempts before it (the s3 not-exist sentinel, see #153's record) were both clean refusals with zero partial state — the refuse-first posture this issue mandated proved itself before it was even fully in play. .migrated files remain on the PVC as the one-release fallback.

Shipped in **v0.5.2–v0.5.4** with #153 and executed live on the dogfood 2026-08-09. `statestore.MigrateFile` implements the policy exactly as specced: local-only → upload + retire to `.migrated`; backend-only/fresh → no-op; identical (byte compare) → retire; **differing → refuse to start** with both SHA-256 digests and the exact operator instruction (rename local to prefer backend, delete object to prefer local). Newest-wins rejected as designed — holds. Idempotent per name; runs in the bind loop before anything consumes state. All four cases unit-tested. The acceptance run happened for real: the dogfood's live state — license, settings, shares, holds, peering trust, watermarks — migrated into the Garage bucket on the v0.5.4 boot. Evidence: `instance peering … enabled=true` in the boot log (that flag only exists in the migrated `settings.json`, read back from the bucket and decrypted). Two crashlooped deploy attempts before it (the s3 not-exist sentinel, see #153's record) were both **clean refusals with zero partial state** — the refuse-first posture this issue mandated proved itself before it was even fully in play. `.migrated` files remain on the PVC as the one-release fallback.
Cordy closed this issue 2026-08-09 04:45:44 +00:00
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#154
No description provided.