Skip to content

fix: SQLite busy_timeout per-connection via DSN (stop SQLITE_BUSY recurrence)#14

Merged
ipiton merged 2 commits into
mainfrom
bugfix/sqlite-busy-timeout-per-connection
May 23, 2026
Merged

fix: SQLite busy_timeout per-connection via DSN (stop SQLITE_BUSY recurrence)#14
ipiton merged 2 commits into
mainfrom
bugfix/sqlite-busy-timeout-per-connection

Conversation

@ipiton

@ipiton ipiton commented May 23, 2026

Copy link
Copy Markdown
Owner

Проблема

index_documents периодически падал с database is locked (5) (SQLITE_BUSY), несмотря на фикс инцидента 2026-05-05 (T59, internal/dbutil). Логи: 2026-05-22 14:54 delete chunks, 2026-05-23 00:24 upsert chunk (trigger: file_watcher).

Корень

dbutil.OpenSQLite ставил pragma через db.Exec после Open. journal_mode=WAL персистентен на уровне файла БД (поэтому WAL и заработал), но busy_timeoutper-connection: один Exec конфигурирует только то соединение пула, что его обслужило. Остальные соединения, которые database/sql открывает под конкурентную работу, получали busy_timeout=0 → мгновенный SQLITE_BUSY при writer-контеншене (фоновый file-watcher индекс против записи).

Доп.: исходный ?_journal_mode=WAL — несуществующий параметр modernc.org/sqlite (драйвер знает только _pragma=...), отсюда rollback-journal на диске в первом инциденте.

Фикс

OpenSQLite передаёт pragma через DSN:

_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)&_txlock=immediate
  • _pragma=modernc выполняет PRAGMA ... на каждом новом соединении пула (Driver.Open), busy_timeout доходит до всех.
  • _txlock=immediate → writer берёт write-lock на BEGIN. busy_timeout не вызывается, когда deferred-транзакция апгрейдит read→write lock при занятом writer'е (SQLite сразу возвращает SQLITE_BUSY, минуя busy handler); BEGIN IMMEDIATE это закрывает.

Регресс-тест TestOpenSQLite_BusyTimeoutPerConnection проверяет timeout на втором соединении пула.

Проверено

  • go build ./..., go vet, go test ./... — всё зелёное.

Follow-ups (в 06-planning/2026-05-05-sqlite-busy-incident.md §7)

  • RC5 (tech-debt): foreground callIndexDocuments зовёт IndexDocuments мимо guard re.indexing, которым пользуются startup и file_watcher → возможна параллельная запись. RC4-фикс делает их вежливой очередью (ждут до 5с), но при write-txn >5с BUSY теоретически остаётся. Нужен единый writer-guard (меняет семантику) — отдельная задача.
  • Embedding-таймаут: тяжёлый индекс упирается в HTTP-таймаут к bge-m3 (:8090) — не связано с БД, отдельно.

ipiton added 2 commits May 23, 2026 20:45
…recurrence

busy_timeout is a per-connection setting, but OpenSQLite applied it with a
single db.Exec after Open — only the one pooled connection that served the
Exec got it. journal_mode=WAL persists at the file level (so WAL engaged via
the 0.8.0 fix), but every other pool connection database/sql opened for
concurrent work defaulted to busy_timeout=0 and returned SQLITE_BUSY instantly
under writer contention (file-watcher index racing a write).

Pass the pragmas through the DSN _pragma parameter instead: modernc.org/sqlite
runs each one as PRAGMA ... on every new pool connection (Driver.Open). Add
_txlock=immediate so writers take the write lock at BEGIN — busy_timeout is not
honored when a deferred transaction fails to upgrade a read lock to a write
lock (SQLite returns SQLITE_BUSY immediately without invoking the busy handler).

The previous ?_journal_mode=WAL DSN form was a non-existent modernc parameter
(the driver only honors _pragma=...), which is why the rollback journal showed
up on disk in the original incident. Regression test asserts busy_timeout holds
on a second pooled connection.
@ipiton ipiton merged commit d3b3bd6 into main May 23, 2026
1 check passed
@ipiton ipiton deleted the bugfix/sqlite-busy-timeout-per-connection branch May 23, 2026 16:47
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