micro / micro/mu

Data integrity: finish backup recovery, restore verification and store safeguards

Open
#1,458 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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 backup producing 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.

  • writeAtomic rotates: x.jsonx.json.1x.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.json already 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.