Wait on a busy SQLite database and turn on WAL mode (closes #198)
check / check (push) Failing after 2s
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:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user