The 3 GiB page cache and temp_store=MEMORY were applied once in Initialize, so only one pooled connection carried them and the other nine got no busy_timeout — the likely source of the many database is locked errors. That memory is C heap the Go runtime cannot see.
What changed, in internal/database/database.go:
The per-connection settings now live in the DSN, so every one of the ten pooled connections gets them: _cache_size=-65536 (64 MiB each, 640 MiB worst case for the pool), _synchronous=OFF, _busy_timeout=5000, _journal_mode=WAL.
cache_size, temp_store=MEMORY, synchronous and busy_timeout are gone from the Initialize pragma list. With temp_store back at its default, the DISTINCT temp B-trees spill to disk instead of growing the C heap.
Two process-wide pragmas are set once: soft_heap_limit 1 GiB and hard_heap_limit 1.5 GiB. At the hard limit a statement returns SQLITE_NOMEM; the batch write paths already log the error and drop the batch, so nothing panics or exits.
The four numbers are named constants, each with a one-line comment.
What the diff does not show: I confirmed the drop-on-error behaviour by reading the batch callers (internal/routewatch/prefixhandler.go and the ASN/peer handlers) — each logs and continues.
The test holds five pooled connections open at once and asserts on each: cache_size -65536, busy_timeout 5000, temp_store not 2, and the process-wide hard_heap_limit.
Disclosure: host make lint/make check cannot run here — the host Go is 1.26 and the pinned linter is built for 1.25, so it panics on load; the authoritative gate is the Docker build (script/cibuild), whose lint, fmt-check and test stages ran fresh and green.
Model: opus-4-8
Part of https://git.eeqj.de/sneak/routewatch/issues/3 (unit U2); closes https://git.eeqj.de/sneak/routewatch/issues/8.
The 3 GiB page cache and `temp_store=MEMORY` were applied once in `Initialize`, so only one pooled connection carried them and the other nine got no `busy_timeout` — the likely source of the many `database is locked` errors. That memory is C heap the Go runtime cannot see.
What changed, in `internal/database/database.go`:
- The per-connection settings now live in the DSN, so every one of the ten pooled connections gets them: `_cache_size=-65536` (64 MiB each, 640 MiB worst case for the pool), `_synchronous=OFF`, `_busy_timeout=5000`, `_journal_mode=WAL`.
- `cache_size`, `temp_store=MEMORY`, `synchronous` and `busy_timeout` are gone from the `Initialize` pragma list. With `temp_store` back at its default, the `DISTINCT` temp B-trees spill to disk instead of growing the C heap.
- Two process-wide pragmas are set once: `soft_heap_limit` 1 GiB and `hard_heap_limit` 1.5 GiB. At the hard limit a statement returns `SQLITE_NOMEM`; the batch write paths already log the error and drop the batch, so nothing panics or exits.
- The four numbers are named constants, each with a one-line comment.
What the diff does not show: I confirmed the drop-on-error behaviour by reading the batch callers (`internal/routewatch/prefixhandler.go` and the ASN/peer handlers) — each logs and continues.
The test holds five pooled connections open at once and asserts on each: `cache_size` -65536, `busy_timeout` 5000, `temp_store` not 2, and the process-wide `hard_heap_limit`.
Disclosure: host `make lint`/`make check` cannot run here — the host Go is 1.26 and the pinned linter is built for 1.25, so it panics on load; the authoritative gate is the Docker build (`script/cibuild`), whose lint, fmt-check and test stages ran fresh and green.
Model: opus-4-8
The 3 GiB page cache and temp_store=MEMORY were set once in Initialize, so
only one pooled connection carried them and the other nine got no busy_timeout,
which drove many "database is locked" errors. None of that memory was visible
to the Go runtime.
Move the per-connection settings into the DSN so every pooled connection gets a
64 MiB cache (640 MiB worst case over ten connections), synchronous OFF, a
5 s busy_timeout and WAL. Drop cache_size, temp_store, synchronous and
busy_timeout from the Initialize pragmas; DISTINCT temp B-trees now spill to
disk. Add process-wide soft (1 GiB) and hard (1.5 GiB) heap limits; at the hard
limit a statement returns SQLITE_NOMEM and the existing batch paths already log
and drop, so nothing panics or exits.
Model: opus-4-8
PASS — this bounds the SQLite page cache and heap across the entire ten-connection pool exactly as #8 requires (per-connection settings moved to the DSN, -3145728 gone, cache_size/temp_store/synchronous/busy_timeout dropped from Initialize, process-wide soft_heap_limit and hard_heap_limit added as named constants), the pragma test holds five pooled connections open and meaningfully asserts each setting, and the authoritative Docker gate is green.
Disclosure: PR body (271 words) and commit body (124 words) sit just over the ~250/~120 guides; judged non-blocking.
Model: opus-4-8
PASS — this bounds the SQLite page cache and heap across the entire ten-connection pool exactly as https://git.eeqj.de/sneak/routewatch/issues/8 requires (per-connection settings moved to the DSN, `-3145728` gone, `cache_size`/`temp_store`/`synchronous`/`busy_timeout` dropped from `Initialize`, process-wide `soft_heap_limit` and `hard_heap_limit` added as named constants), the pragma test holds five pooled connections open and meaningfully asserts each setting, and the authoritative Docker gate is green.
Disclosure: PR body (271 words) and commit body (124 words) sit just over the ~250/~120 guides; judged non-blocking.
Model: opus-4-8
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #3 (unit U2); closes #8.
The 3 GiB page cache and
temp_store=MEMORYwere applied once inInitialize, so only one pooled connection carried them and the other nine got nobusy_timeout— the likely source of the manydatabase is lockederrors. That memory is C heap the Go runtime cannot see.What changed, in
internal/database/database.go:_cache_size=-65536(64 MiB each, 640 MiB worst case for the pool),_synchronous=OFF,_busy_timeout=5000,_journal_mode=WAL.cache_size,temp_store=MEMORY,synchronousandbusy_timeoutare gone from theInitializepragma list. Withtemp_storeback at its default, theDISTINCTtemp B-trees spill to disk instead of growing the C heap.soft_heap_limit1 GiB andhard_heap_limit1.5 GiB. At the hard limit a statement returnsSQLITE_NOMEM; the batch write paths already log the error and drop the batch, so nothing panics or exits.What the diff does not show: I confirmed the drop-on-error behaviour by reading the batch callers (
internal/routewatch/prefixhandler.goand the ASN/peer handlers) — each logs and continues.The test holds five pooled connections open at once and asserts on each:
cache_size-65536,busy_timeout5000,temp_storenot 2, and the process-widehard_heap_limit.Disclosure: host
make lint/make checkcannot run here — the host Go is 1.26 and the pinned linter is built for 1.25, so it panics on load; the authoritative gate is the Docker build (script/cibuild), whose lint, fmt-check and test stages ran fresh and green.Model: opus-4-8
PASS — this bounds the SQLite page cache and heap across the entire ten-connection pool exactly as #8 requires (per-connection settings moved to the DSN,
-3145728gone,cache_size/temp_store/synchronous/busy_timeoutdropped fromInitialize, process-widesoft_heap_limitandhard_heap_limitadded as named constants), the pragma test holds five pooled connections open and meaningfully asserts each setting, and the authoritative Docker gate is green.Disclosure: PR body (271 words) and commit body (124 words) sit just over the ~250/~120 guides; judged non-blocking.
Model: opus-4-8