Files
Maven/docs/evals/2026-08-07-store-connection-cap.md
T
claude af4eeceb6a Keep the store's one connection, delete the seam it cannot survive (V-642)
`SetMaxOpenConns(1)` under WAL gives up concurrent reads, and the task
asked whether that costs anything. Measured over a fixed two-second
window, a paced writer against a read loop, three runs per cap:
reads do not queue. Four connections buy 70µs at p50 on a turn that
spends 1.19s in the resident model, and write throughput more than
halves. A 19ms worst case also cannot be the source of the 2.7s router
figure, so that line of enquiry is closed.

What the cap cannot survive is a long-lived transaction. It holds the
only connection, so a second read never completes: two seconds and
`context deadline exceeded`, against 1ms at a cap of four.

`Store.DB` handed out exactly that transaction. It had been there since
the initial commit with no production caller, and its comment described
a loop that never materialised. Its one user was a test helper reading
`delivery_attempts` by raw SQL, which `ListDeliveryAttempts` has covered
since V-390. So the cap stays and the seam goes, and the hazard is gone
by construction rather than by documentation.

`internal/store/conncap_test.go` stays as the standing measurement,
skipped under -short. The comment at the cap and the one in
`internal/ipc/server.go` that leans on it now state the invariant and
cite the numbers.

Measurement: docs/evals/2026-08-07-store-connection-cap.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:01:27 +04:00

3.3 KiB

Does one sqlite connection make reads queue? No (V-642)

Measured 07-08-2026 at 7b507de, on homesrv. The harness is internal/store/conncap_test.go. It stays in the repo, because this claim gets re-argued and the numbers should be re-runnable rather than quoted.

internal/store/store.go opens the database with SetMaxOpenConns(1), while schema.sql sets journal_mode=WAL. WAL exists to let readers run beside one writer, so the cap gives up the thing the journal mode was chosen for. The question was whether that costs anything.

What was measured

A fixed two-second window. One writer calling SetValue paced at 2ms, and a reader loop calling RecentFacts(50) over 500 seeded rows as fast as it can. Same schema, same modernc driver, same machine, three runs per cap.

The window is wall-clock rather than a read count on purpose. A first version ran a fixed 300 reads. That finished sooner at the higher cap, so it received fewer writes, and two runs that did different work cannot be compared.

cap reads writes p50 p95 max
1 ~3050 ~760 594µs 900µs 16-19ms
4 ~3600 ~340 525µs 710µs 1-2ms

What it says

Reads do not queue behind writes. Four connections buy about 70µs at p50. A turn spends 1.19s in the resident model. The tail does improve, from 19ms to 2ms, and 19ms is still not a figure anyone notices in a spoken reply.

Write throughput more than halves at the higher cap, 760 writes against 340. inference, not measured directly: at one connection the reader and the writer take turns with no lock contention. At four the writer contends for the WAL write lock with a live reader. Whatever the mechanism, the trade runs the opposite way from the one the task expected.

The cap was not the source of the 2.7s router figure. CLAUDE.md records that figure as contention rather than the model. This task was a candidate for where that contention came from. A 19ms worst case cannot produce it. That line of enquiry is closed.

One transaction is what the cap cannot survive. With a read-only transaction open, a second read at cap 1 never completes. The harness gave it two seconds and got context deadline exceeded. The same read at cap 4 took 1ms. The transaction holds the only connection, so this is not a slow read, it is a stalled database.

What was done

The cap stays at 1. The reason is now written where the cap is set, rather than inferred from a four-word comment.

Store.DB was deleted. It handed out exactly the read-only transaction measured above. It had been there since the initial commit with no production caller, and its doc comment described a loop that never materialised. Its one user was a test helper reading delivery_attempts by raw SQL. ListDeliveryAttempts has covered that since V-390, and the helper now goes through the reader.

So the hazard is gone by construction, not by documentation. TestConnCap_ReadBlocksBehindOpenSnapshot is the standing measurement of what re-adding the seam would cost.

Not answered

Whether reads queue on the deployed box under real load, as opposed to a synthetic loop. The harness writes and reads one table. Digestion reads four and embeds while it does. The finding that closes this task is the transaction stall, which is structural and does not depend on load.