
De fout die geen bug in mijn code was
Donderdagochtend lichtte mijn Telegram op met steeds dezelfde boodschap: “I encountered an unexpected error.” Niet van een gebruiker — van mijn eigen agent-gateway. Elk bericht dat ik stuurde kwam kapot terug. Toen ik in de logs dook, was de fout bot en definitief: sqlite3.DatabaseError: file is not a database. Mijn sessie-database, het bestand met 637.000 berichten verdeeld over 21.000 gesprekken, was opgehouden een database te zijn.
Wat volgde was een debug-sessie die me iets leerde wat ik jaren eerder had moeten weten: het gevaarlijkste wat je een SQLite-database kunt aandoen, is het proces killen dat er net naar schrijft.
De keten van gebeurtenissen
Ik trok het stap voor stap terug. Om 08:34 stopte een update-pipeline de gateway met een nette SIGTERM. De gateway begon correct af te sluiten — hij heeft veel MCP-servers en actieve sessies, dus hij heeft tijd nodig. Maar mijn systemd-config gaf hem maar 210 seconden. Om 08:38 liep de shutdown uit, en systemd deed wat systemd doet: het stuurde een SIGKILL naar de hele processenboom. Dertig-plus processen, in één klap weg.
De SIGKILL landde midden in een WAL-write. SQLite’s write-ahead log was halverwege een checkpoint, en de harde kill corrumpeerde de database-header. Bij de herstart ving de eigen integriteitscheck het op: “state.db FAILED integrity check after an unclean gateway exit.”
En hier is het deel dat stak: dit was niet de eerste keer. Het was de dag ervoor ook al gebeurd. Mijn eigen restart-scripts riepen systemctl kill -s SIGKILL aan als hun eerste actie — niet als laatste redmiddel. Ik had het gevaarlijke ding met mijn eigen database gedaan, keer op keer, en was vervolgens verbaasd dat hij brak.
De fix was saai, en dat is precies het punt
Geen slimme nieuwe tool nodig. Twee veranderingen, allebei saai:
- Geef de gateway de tijd om netjes te sterven. Ik verhoogde
TimeoutStopSecvan 210 naar 300 seconden, zodat SIGTERM de kans krijgt om de WAL te checkpointen vóór er iets geforceerd wordt. - SIGTERM eerst, SIGKILL alleen als fallback. Ik herschreef drie restart-scripts zodat ze de service netjes stoppen en 45–60 seconden wachten vóór ze ooit een harde kill overwegen.
Het herstel zelf was bijna anticlimactisch. PRAGMA integrity_check kwam schoon terug — geen dataverlies. De echte opruiming was het verwijderen van drie byte-identieke kopieën van de corrupte staat, elk 3,4 GB, die mijn herstelproces behulpzaam had aangemaakt. Dertien gigabyte aan rommel, weg.
De diepere les is er een die ik in verschillende vormen blijf herleren: een gedocumenteerde fix is geen fix. Ik had de juiste manier om een gateway te herstarten al eerder opgeschreven. De scripts deden nog steeds het verkeerde. Het enige dat het gedrag daadwerkelijk veranderde, was verifiëren dat de code op disk overeenkwam met wat ik dacht dat er stond.
Wat ik iedereen zou vertellen die SQLite in productie draait
Als je SQLite met WAL-mode draait — en dat doe je waarschijnlijk — behandel SIGKILL dan als laatste redmiddel, nooit als eerste zet. Stuur altijd SIGTERM en wacht op een nette checkpoint. Maak je shutdown-timeout langer dan je werkelijke shutdown-tijd, want die tijd groeit naarmate je meer services toevoegt. En als je “file is not a database” ziet, check dan de integriteit van het huidige bestand vóór je een backup vertrouwt — de kopieën die je herstelproces maakt zijn vaak byte-identieke kopieën van de corrupte staat, geen bruikbare backups.
Heb jij ooit een proces gekild en een database stilletjes zien corrumperen? Ik hoor graag hoe jij het hebt hersteld — de reacties staan open.
Wat vond je van dit bericht?