From 047945cabf264aecfe15d201a2facafaf08865fb Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Thu, 1 Oct 2026 22:16:14 +0200 Subject: [PATCH] Make concurrent database writes wait instead of failing (closes #253) SQLite transactions started deferred, so two that both read and then wrote could not both proceed, and SQLite refused the second at once with "database is locked" without waiting. TestCreateUserRaceCondition hit this now and then. The database is now opened with _txlock=immediate, so each transaction takes the write lock when it begins and a second one waits for it, and with _busy_timeout=5000, so that wait lasts up to 5 seconds. The driver already defaults to 5000; it is now stated in the code. Model: opus-5-5 --- TODO.md | 4 ++++ internal/database/database.go | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index ba79bcf..1dc2403 100644 --- a/TODO.md +++ b/TODO.md @@ -20,6 +20,10 @@ regress. # Completed Steps +- 2026-10-01: Two database writes at the same moment no longer fail with + "database is locked": each transaction now takes the write lock when it begins + and waits up to 5 seconds for another writer to finish (#253). + - 2026-10-01: The hint under the app page's environment variable editor now says changes take effect at the next deploy or rollback, in the page's warning style, instead of asking for a container restart, which keeps the old values diff --git a/internal/database/database.go b/internal/database/database.go index 929ef5e..75cc220 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -137,8 +137,11 @@ func (d *Database) connect(ctx context.Context) error { return fmt.Errorf("failed to create data directory: %w", err) } - // Open database with WAL mode and foreign keys - dsn := dbPath + "?_journal_mode=WAL&_foreign_keys=on" + // Open database with WAL mode and foreign keys. Transactions take the + // write lock when they begin and wait up to 5s for another writer, + // instead of failing with "database is locked" when both write. + dsn := dbPath + "?_journal_mode=WAL&_foreign_keys=on" + + "&_txlock=immediate&_busy_timeout=5000" database, err := sql.Open("sqlite3", dsn) if err != nil {