Skip to content

feat(storage): SQLite read-pool to alleviate writer contention#88

Open
aksOps wants to merge 1 commit intomainfrom
feat/sqlite-read-pool
Open

feat(storage): SQLite read-pool to alleviate writer contention#88
aksOps wants to merge 1 commit intomainfrom
feat/sqlite-read-pool

Conversation

@aksOps
Copy link
Copy Markdown
Contributor

@aksOps aksOps commented May 5, 2026

Summary

  • Open a second *gorm.DB against the SQLite file with MaxOpen=N (default 4, range [1,32]) and PRAGMA query_only=ON. Read handlers route through a new r.reader() accessor; writes stay on r.db (which keeps MaxOpen=1 per SQLite's single-writer model).
  • Eliminates read tail-latency spikes when API/MCP queries queue behind retention/VACUUM/in-flight writes on the writer pool.
  • Configurable via DB_SQLITE_READ_POOL_SIZE (default 4; 0/off/false/no disables and falls back to writer). Non-SQLite drivers ignore the flag.
  • WAL is already enabled, so each read-pool connection has its own snapshot and reads don't block writers.

Changes

  • New: internal/storage/factory.go::NewSQLiteReadPool(dsn, maxOpen) factory.
  • internal/storage/repository.go: readPool field + reader() accessor, env parser, NewRepository wiring (SQLite-only, graceful fallback on factory failure), Close() closes both pools (read-pool errors logged via slog.Warn).
  • internal/storage/{log,trace,metrics,graph,archive}_repo.go: every read-only path routed through reader(); writes (BatchCreate*, Update*, Purge*, Vacuum, FTS5/DDL) unchanged.
  • New: internal/storage/sqlite_read_pool_test.go: env parsing, query_only=ON INSERT-rejection, reader() identity fallback, concurrent reads + writes with p99 latency assertion.
  • CLAUDE.md: DB_SQLITE_READ_POOL_SIZE documented.

Test plan

  • `go vet ./...` clean
  • `go build ./...` clean
  • `go test -timeout 180s -race ./internal/storage/...` passes (8.9s)
  • Concurrent test asserts `repo.reader() == rp` (identity) and p99 read latency < 200ms while writer pumps inserts (observed: n=160, p50=7.46ms, p99=22.85ms, max=26.21ms)
  • Non-SQLite drivers verified untouched (read pool only opened when `driver == "sqlite"`)
  • Code review applied (6 items)

🤖 Generated with Claude Code

SQLite hardcodes MaxOpen=1 (correct: SQLite has a single-writer model), but
the Go driver pool serializes ALL operations through that one connection.
API/MCP read handlers were queueing behind in-flight writes/retention/VACUUM,
producing slow tail latency under load.

This change opens a separate *gorm.DB against the same file with MaxOpen=N
(default 4, range [1,32]) and PRAGMA query_only=ON. Read-only Repository
methods route through a new r.reader() accessor; writes (BatchCreate*,
Update*, Purge*, VacuumDB, FTS5/DDL) stay on r.db. WAL mode is already
enabled, so each read connection has its own snapshot and reads do not
block writers.

- New: internal/storage/factory.go: NewSQLiteReadPool(dsn, maxOpen).
- internal/storage/repository.go: readPool field, reader() accessor,
  sqliteReadPoolSizeFromEnv() parser, NewRepository wiring (SQLite-only,
  graceful fallback on factory failure), Close() now closes both pools.
- internal/storage/{log,trace,metrics,graph,archive}_repo.go: every
  read-only path routed through reader(); writes unchanged.
- New: internal/storage/sqlite_read_pool_test.go: env parsing, query_only
  INSERT-rejection, reader() identity fallback, concurrent reads + writes
  with p99 latency assertion (<200ms; observed 22.85ms).
- CLAUDE.md: DB_SQLITE_READ_POOL_SIZE documented.

Non-SQLite drivers ignore the setting; reader() falls back to the writer
when readPool is nil. NewRepositoryFromDB does not open a read pool — its
doc now notes this so test authors aren't surprised by SQLite contention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant