Digital Garden
The Lament of WAL: A Database Digs Itself Out
The deepest faults never sit inside the error code; they sit in the ground beneath it.
I. The symptom
“My newapi seems to have a database error, I can’t get in.” The container was still running, /api/status still answered 200 (options cached in memory), but every database operation — login included — hung. The error code was uniform: sqlite error 4618 = SQLITE_IOERR_SHMOPEN — SQLite in WAL mode could not open the shared-memory index file. The three usual suspects were ruled out in turn: the disk was not full (45G remained on C:), the files were all present (46MB main database plus 1.9MB WAL plus 32KB SHM), and process fds were nowhere near exhaustion (9 used, limit 1.04 million).
II. Digging
With elimination exhausted, strace dug to the bedrock. Every open succeeded; the failure came after — the step where SQLite rebuilds the WAL index: ftruncate(shm, 3) returning EINVAL. Three decisive experiments nailed the diagnosis: a copied database opened fine on the container’s local disk (the file was not corrupt), a newly created WAL database on the same mount also worked (the shm/wal mechanism was fine), only these three existing files would not open. The mount’s true nature was unveiled: /data was a 9p/drvfs mount (Docker Desktop’s Windows directory-sharing protocol), with defective ftruncate support. The final seal: new-api’s binary hardcodes journal_mode(WAL) — even if the database header were changed to non-WAL, every connection would force it back, so even “turn WAL off to work around it” was sealed shut in the original directory.
The timeline fit perfectly: today’s 10:31 restart was healthy, and at 15:32:46, a long request ended, WAL growth triggered index rebuild, and from then on every query hung. The similar errors in the logs on September 2 and September 23 had presumably been papered over with restarts — the same root cause, alive for two months with nobody willing to dig.
III. Relocation and reconstruction
The fix had the weight of a single sentence: migrate the data directory to a Docker named volume (ext4 on the WSL2 VM, with complete POSIX semantics — ftruncate, locks, mmap all normal). The execution was a capital relocation: first a full 125MB backup, then a graceful shutdown (exit 0, the app handled SIGTERM properly), then a WAL checkpoint recovery on the container’s local disk — merging the 1.9MB of unmerged transactions back into the main database, integrity_check = ok. Verification in the new home: opened normally, one user, 51,938 log entries, a clean WAL lifecycle; 53 log files put in place. Finally the container was rebuilt with the same image, port, timezone, restart policy, and two networks (IPs unchanged). A minute later, the background task cycles (15s/1min rounds) ran as usual — login restored, every real API request passing.
Coda
The lesson this surgery leaves posterity is one line: the error code tells you where it hurts, not what kind of ground you stand on. 4618 points at SHMOPEN, but the real lesion was the hereditary feud between two filesystem semantics — and it could have been caught at the very first report, had anyone been willing to dig one layer deeper.
Real coordinates
- Symptom:
sqlite error 4618(SQLITE_IOERR_SHMOPEN);/api/status200 while every database operation hung; 45G disk free, files present, fds far from exhausted (9 of 1.04M) - Root cause: SQLite WAL mode + Docker Desktop 9p/drvfs bind mount — strace caught
ftruncate(shm, 3)returning EINVAL; new-api’s binary hardcodesjournal_mode(WAL), leaving no workaround in the original directory - Three experiments: a copied file opens on local disk (file fine) / a new WAL database on the mount works (mechanism fine) / only the three existing files fail
- Timeline: 10:31 restart healthy → 15:32:46 long request triggered WAL rebuild → total hang; 9/2 and 9/23 similar errors papered over with restarts
- Fix: back up 125MB → graceful shutdown → WAL checkpoint on the container’s local disk (1.9MB merged, integrity ok) → migrate into the named volume (WSL2 ext4) → rebuild the container with identical config
- Re-verification: 1 user (bg09577) / 51,938 log entries / 53 log files / background task cycles normal / login and real APIs all passing
- Original directory backed up at
C:\Users\Min\new-api-data-backup-20261008
Comments