Wait on a busy SQLite database and turn on WAL mode (closes #198)
check / check (push) Failing after 2s

Requests and the eviction pass write on separate connections, and with
no busy timeout a write that met another one failed at once with
"database is locked" and was lost. internal/database now adds
_pragma=busy_timeout(5000) to every db_url, the default or one the
operator sets, so such a write waits up to five seconds. The default
db_url's _journal_mode=WAL is not a parameter the driver reads, so it
is now _pragma=journal_mode(WAL). README.md and config.example.yml say
what pixa adds to db_url.

Model: opus-5-5
This commit is contained in:
2026-10-04 18:32:44 +00:00
parent 11d0393704
commit 56f42c4cce
5 changed files with 31 additions and 5 deletions
+6
View File
@@ -490,6 +490,12 @@ Key settings in more detail:
waits for an upstream connection and for a processing slot (up to 10 seconds waits for an upstream connection and for a processing slot (up to 10 seconds
each), so keep it longer than `upstream_fetch_timeout` plus 20 seconds each), so keep it longer than `upstream_fetch_timeout` plus 20 seconds
- `signing_key` — HMAC secret for URL signatures - `signing_key` — HMAC secret for URL signatures
- `db_url` — the SQLite database to open; omitted, it is
`file:<state_dir>/state.sqlite3?_pragma=journal_mode(WAL)`, which keeps the
database in WAL mode. pixa adds `_pragma=busy_timeout(5000)` to any `db_url`,
so a write that finds another in progress waits up to five seconds for it
instead of failing. WAL mode comes only from the URL: keep
`_pragma=journal_mode(WAL)` in one you set
- `cache_max_bytes` — disk cache size limit in bytes; `0` disables the - `cache_max_bytes` — disk cache size limit in bytes; `0` disables the
disk cache entirely; omitted defaults to 75% of the sum of the free space on disk cache entirely; omitted defaults to 75% of the sum of the free space on
the filesystem containing `<state_dir>/cache/` and the bytes of source and the filesystem containing `<state_dir>/cache/` and the bytes of source and
+6
View File
@@ -31,6 +31,12 @@ P2: security: referer blacklist
# Completed Steps # Completed Steps
- 2026-10-04 SQLite writes no longer fail with "database is locked" (closes
#198): pixa adds `_pragma=busy_timeout(5000)` to every `db_url`, so a write
that finds another in progress on another connection waits up to five seconds
for it, and the default `db_url` turns on WAL mode with
`_pragma=journal_mode(WAL)`. The old default's `_journal_mode=WAL` is not a
parameter the driver reads, so the database was never in WAL mode.
- 2026-10-04 `TestPeriodicReconciliationAdoptsFileThatAppearsAfterStartup` - 2026-10-04 `TestPeriodicReconciliationAdoptsFileThatAppearsAfterStartup`
only passes through a periodic pass (closes #189): it slept for three only passes through a periodic pass (closes #189): it slept for three
eviction intervals before writing its file, and a startup pass still running eviction intervals before writing its file, and a startup pass still running
+4 -3
View File
@@ -34,9 +34,10 @@ maintenance_mode: false
state_dir: ./data state_dir: ./data
# SQLite database URL (default: # SQLite database URL (default:
# file:<state_dir>/state.sqlite3?_journal_mode=WAL). An empty value aborts # file:<state_dir>/state.sqlite3?_pragma=journal_mode(WAL)). pixa adds
# startup; leave the key out to use the default. # _pragma=busy_timeout(5000) to it. An empty value aborts startup; leave the
# db_url: "file:./data/state.sqlite3?_journal_mode=WAL" # key out to use the default.
# db_url: "file:./data/state.sqlite3?_pragma=journal_mode(WAL)"
# Image proxy settings # Image proxy settings
# HMAC signing key for URL signatures (required, at least 32 characters) # HMAC signing key for URL signatures (required, at least 32 characters)
+4 -1
View File
@@ -320,7 +320,10 @@ func newFromSmartConfig(sc *smartconfig.Config) (*Config, error) {
settingName(keyDBURL), errValueEmpty) settingName(keyDBURL), errValueEmpty)
} }
c.DBURL = fmt.Sprintf("file:%s/state.sqlite3?_journal_mode=WAL", c.StateDir) // The driver sets the journal mode only through a _pragma
// parameter. WAL lets reads go on while a write is in progress.
c.DBURL = fmt.Sprintf(
"file:%s/state.sqlite3?_pragma=journal_mode(WAL)", c.StateDir)
} }
if loader.err != nil { if loader.err != nil {
+11 -1
View File
@@ -243,7 +243,17 @@ func (s *Database) DB() *sql.DB {
} }
func (s *Database) connect(ctx context.Context) error { func (s *Database) connect(ctx context.Context) error {
dbURL := s.config.DBURL // Requests and the eviction pass write on separate connections. With
// a busy timeout, a write that finds another one in progress waits up
// to five seconds for it instead of failing at once with "database is
// locked". The driver runs each _pragma parameter on every connection
// it opens.
separator := "?"
if strings.Contains(s.config.DBURL, "?") {
separator = "&"
}
dbURL := s.config.DBURL + separator + "_pragma=busy_timeout(5000)"
s.log.Info("connecting to database", "url", dbURL) s.log.Info("connecting to database", "url", dbURL)