Data integrity: finish backup recovery, restore verification and store safeguards
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 434
- Forks
- 21
- Avg merge
- 18m
- Merged PRs (30d)
- 326
Description
Current status — 18 September 2026
Partial implementation now exists: internal/backup provides bounded local snapshots and S3 backup support, and internal/data preserves a .prev copy before a large store shrink. The original claim that no backups exist is obsolete. This is not equivalent to all requested retention/rotation/refusal semantics, cross-store consistency or a verified off-box restore. Keep this open for recovery verification and remaining safeguards; no production backup/restore drill was performed during cleanup.
What happened
wallets.json was written by two different maps — the credit ledger (wallets) and the key store (userWallets) — because a rename moved the second onto a filename the first already owned. Each save replaced the other's contents wholesale. A live account's 560 credits read zero, and every wallet lost its address and private key from that file.
Fixed in 56bef80. Nothing was lost, but only by luck:
- the keys survived in
trade_wallets.json, which is only ever read, never written - the balances survived in
transactions.json, which neither writer touches and which happens to record the balance after every transaction
Neither was a designed safety net. Change either accident and the loss is permanent, because the only backup is a VM snapshot from the previous day.
Why it was possible
Every store is a whole-file JSON blob, rewritten in full on each change. internal/data.writeAtomic makes that crash-safe — temp file, fsync, rename — so a power cut cannot truncate a file. It does nothing about a logic error: a write of the wrong map is atomic, durable, and complete.
So the blast radius of any bug touching a store is the entire store, in one write, with no history.
Plan, in priority order
1. Backups — nothing exists today
-
mu backupproducing a timestamped tar.gz of~/.mu/data - Scheduled daily, and automatically before a deploy or restart, which is when new code first touches the stores
- Off-box copy — the
S3_*settings are already configured for file storage and can hold these - Retention: daily for a fortnight, weekly for a quarter
-
mu restore <archive>and a documented drill, because an untested backup is not a backup
2. Keep the previous generation of every store
Highest value per line of code. These files are small; keeping the last few versions is nearly free.
-
writeAtomicrotates:x.json→x.json.1→x.json.2, keeping the last 3 - Turns "destroyed" into "copy the previous generation back"
- Would have made today a thirty-second recovery instead of an investigation
3. Refuse a write that destroys a store
The check that would have prevented this outright, rather than caught it afterwards.
- Before saving, compare the new record count against what is on disk
- Refuse when a non-empty store would become empty, or shrinks by more than a set proportion, unless the caller explicitly opts in
- Log loudly and keep the old file
- Today's write was an all-empty map over ~500 accounts — the crudest possible version of this stops it
4. Detection
- Test that no store file is written from two different values (56bef80)
- Log a record count per store at startup, so a collapse is visible in the log rather than in a support message
- Warn when a store has shrunk substantially since the last run
5. Write down what is reconstructable from what
-
docs/DATA.md: every store, its owner, what depends on it, and what it can be rebuilt from - Balances rebuild from
transactions.json— implemented, and it should be documented rather than rediscovered - Anything with no reconstruction path is a candidate for an append-only log beside it
6. Longer term: shrink the blast radius
Whole-file rewrite is the underlying hazard. Two directions, not both:
- Append-only logs for anything that is money, with state derived from the log.
transactions.jsonalready works this way, which is exactly why it survived. - SQLite with WAL, which gives transactional writes, a real backup command, and no whole-file rewrite. A large change; worth considering when the JSON stores next cause pain.
The honest part
The collision arrived in aa574e8, but I made it destructive by adding a write to GetOrCreateWallet's repair path without checking what else wrote that filename. walletsFile = "wallets.json" was on screen at the time. A one-line grep would have shown two owners.
Item 3 exists because process did not catch it and would not have. The write should have been impossible, not merely inadvisable.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with internal/data.writeAtomic and the existing internal/backup implementation, then trace the store writers and S3 file-storage settings. Review the planned mu backup and mu restore commands and docs/DATA.md. Done means the requested rotation, refusal, recovery verification, logging, retention, and reconstruction documentation are implemented and tested, including a restore drill.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, go
- Domain
- backend, databases, devops
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 28/100