PythonMastery

GitLab deleted its production database, and the backups weren't there

In January 2017 an engineer wiped GitLab.com's primary database by mistake. The daily backups had been failing without anyone knowing. Here is how, and the check that catches it.

What happened

On 31 January 2017, GitLab.com's database replication had stopped, and an engineer was rebuilding the secondary server. That job needs an empty data directory on the secondary, so they deleted it. GitLab's post-mortem describes what went wrong next: they did so "errantly thinking they were doing so on the secondary. Unfortunately this process was executed on the primary instead."

They stopped the command "a second or two after noticing their mistake, but at this point around 300 GB of data had already been removed."

GitLab.com was down for about 18 hours, most of it spent copying data back over slow disks. Changes made in a six-hour window were gone for good: "roughly 5,000 projects, 5,000 comments and 700 new user accounts." Git repositories and wikis were not affected. Only the database was.

Why the backups didn't save them

Running the right command on the wrong server can happen to anyone. What turned it into lost data was that GitLab had several ways to recover, and when they needed them, almost none worked:

The failure you can reproduce

The dangerous part wasn't the error. It was an error that nobody saw. Here's a small version: a backup step that fails, a wrapper that swallows the failure, and a job that reports success anyway:

python
import json

def dump(db, tool_version, server_version):
    if tool_version != server_version:
        raise RuntimeError(f"server version mismatch: server {server_version}, tool {tool_version}")
    return json.dumps(db)

def nightly_backup(db):
    try:
        return dump(db, tool_version="9.2", server_version="9.6")
    except Exception as e:
        # The warning goes somewhere nobody reads. Here: nowhere.
        return ""

db = {"projects": ["api", "docs", "site"], "users": ["ada", "grace"]}
backup = nightly_backup(db)
print(f"backup job finished, {len(backup)} bytes written")
output
backup job finished, 0 bytes written

The job ran and finished, and nothing told anyone it had written nothing.

Test the restore, not the backup

The fix is to stop trusting that a backup exists and prove you can restore it: load it back, and compare what comes back with what you expected.

python
import json

def restore_check(backup, expected):
    if not backup:
        raise RuntimeError("backup is empty")
    restored = json.loads(backup)
    for table, rows in expected.items():
        got = len(restored.get(table, []))
        if got != len(rows):
            raise RuntimeError(f"{table}: restored {got} rows, expected {len(rows)}")
    return "restore OK"

db = {"projects": ["api", "docs", "site"], "users": ["ada", "grace"]}

good = json.dumps(db)
print(restore_check(good, db))

for bad in ["", json.dumps({"projects": ["api"], "users": []})]:
    try:
        restore_check(bad, db)
    except RuntimeError as e:
        print("ALERT:", e)
output
restore OK
ALERT: backup is empty
ALERT: projects: restored 1 rows, expected 3

A check like this fails loudly on the first night the backup breaks, not on the day you need it. GitLab's own action list includes the same idea at full scale: "Automated testing of recovering PostgreSQL database backups".

Fix the system, not the person

GitLab's post-mortem doesn't name the engineer, and its fixes aim at the situation they were in. In GitLab's words, the focus was "to improve disaster recovery, and making it more obvious as to what host you're using; instead of preventing production engineers from running certain commands."

Their list of fifteen follow-ups includes:

What you can take from it

the tipTake this away

A backup you have never restored is a hope, not a backup: restore it automatically and check what comes back. Learn it properly: Exceptions: Patterns Beyond the Basics, File I/O, Pathlib, and Large Files, CSV, JSON, and JSONL — Structured Text Done Right.

Sources

every claim above comes from these