- 91.7%
- file paths in the database pointing at missing files
- 169 to 75 MB
- database size after the fixes
- 156 to 36 s
- sync time
A long-running pipeline of mine keeps its state in two places: a SQLite database and daily JSON files. That was a convenience at the start. Files were easy to write from the ingest step, and the database was easy to query from the dashboard.
When I audited the system, four of the five worst bugs were the same bug.
- 1Ingest writes daily JSON files
- 2A sync copies them into SQLite
- 3Retention job reads one of them
- 4Dashboard reads the other
What went wrong
- Nothing synced the JSON files into the database. One day had 165 records in the files and 2 in the database.
- When I added a sync, it refused to carry the status field. That stranded 708 records that already had a finished output.
- A retention job read the wrong store and moved finished outputs to the trash. 7,324 of 9,399 rows then pointed at a file that no longer existed.
- A read handler was modifying an in-memory array as a side effect. The dashboard showed 18 stale items while the database held none.
None of these is clever. Each one is a reasonable bit of code that assumed the other store agreed with it.
It kept getting worse until I measured
Later I found that 91.7% of the file paths in the database pointed at files that were gone. The list endpoint took about 1.4 seconds, and I had been ready to optimise it. It was slow because it was defending itself against a database that lied to it.
The first reconcile pass cleared 823 stale source paths and 210 stale output paths. After that the endpoint did not need optimising.
A related bug: the event log had 216,798 rows for 6,965 items. One item was duplicated 154 times, because every sync replayed history. Making the insert idempotent got it to 31,556 rows, but a second sync still added 3,454. The fix was to stop replaying at all. The database went from 169 MB to 75 MB and a sync from 156 seconds to 36.
What I do now
- Every column has one owner. Either the machine writes it or a human does, never both.
- Reconcile before you optimise. If a query is slow, first check that the data it reads is true.
- Make syncs idempotent, then make them smaller. Idempotent is not the same as cheap.
If you have a “temporary” second store in a project, this is your sign to pick one.