{"record":{"id":"aa4b06dde708ba69","repo":"benbjohnson/litestream","slug":"reacquire-read-lock-w","errorCode":null,"errorMessage":"reacquire read lock: %w","messagePattern":"reacquire read lock: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"db.go","lineNumber":2676,"sourceCode":"\tdefer func() { _ = db.acquireReadLock(ctx) }()\n\n\t// A non-forced checkpoint is issued as \"PASSIVE\". This will only checkpoint\n\t// if there are not pending transactions. A forced checkpoint (\"RESTART\")\n\t// will wait for pending transactions to end & block new transactions before\n\t// forcing the checkpoint and restarting the WAL.\n\t//\n\t// See: https://www.sqlite.org/pragma.html#pragma_wal_checkpoint\n\trawsql := `PRAGMA wal_checkpoint(` + mode + `);`\n\n\tvar row [3]int\n\tif err := db.db.QueryRowContext(ctx, rawsql).Scan(&row[0], &row[1], &row[2]); err != nil {\n\t\treturn 0, err\n\t}\n\tdb.Logger.Debug(\"checkpoint\", \"mode\", mode, \"result\", fmt.Sprintf(\"%d,%d,%d\", row[0], row[1], row[2]))\n\n\t// Reacquire the read lock immediately after the checkpoint.\n\tif err := db.acquireReadLock(ctx); err != nil {\n\t\treturn 0, fmt.Errorf(\"reacquire read lock: %w\", err)\n\t}\n\n\treturn row[1], nil\n}\n\ntype snapshotReadPosition struct {\n\tpos          ltx.Pos\n\tpageSize     int\n\twalEndOffset int64\n\tdb           *DB\n\tcloseOnce    sync.Once\n}\n\nfunc (p *snapshotReadPosition) close() {\n\tp.closeOnce.Do(func() { p.db.chkMu.RUnlock() })\n}\n\ntype snapshotReadCloser struct {","sourceCodeStart":2658,"sourceCodeEnd":2694,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/db.go#L2658-L2694","documentation":"Immediately after a successful SQLite checkpoint, Litestream re-acquires its read lock (db.acquireReadLock) so ordinary reads/syncs continue to hold it. Failure to reacquire is wrapped with this error and returned from checkpoint(). The checkpoint itself succeeded, but Litestream will not resume normal operation with its read lock held until this succeeds, so the next sync will retry.","triggerScenarios":"db.acquireReadLock(ctx) fails right after issuing the SQLite PRAGMA checkpoint: the ctx was canceled/timed out, another writer grabbed the write lock first, or the internal lock promotion (lock-table insert) fails with SQLITE_BUSY.","commonSituations":"Application holding a long write transaction that blocks the read-lock re-acquire; checkpoint timeout too short for large WALs; process shutting down concurrently.","solutions":["Check the wrapped error: SQLITE_BUSY means a competing writer — add busy_timeout or shorten application write transactions.","If it is a context deadline issue, increase the checkpoint/sync timeout and retry the sync.","Verify no other process is checkpointing the same database (use replica leasing).","Simply retry; the next sync cycle re-attempts lock acquisition automatically."],"exampleFix":"// before\ndb.db.Exec(\"PRAGMA busy_timeout=0\")\n// after\ndb.db.Exec(\"PRAGMA busy_timeout=5000\") // tolerate brief writer contention on reacquire","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"reacquire read lock\") {\n    // checkpoint succeeded; next sync will retry lock acquisition\n    log.Printf(\"read lock reacquire failed, retrying: %v\", err)\n}","preventionTips":["Set PRAGMA busy_timeout to survive writer contention.","Keep application write transactions short.","Increase checkpoint timeouts for large WALs.","Use leasing to prevent competing Litestream processes."],"tags":["read-lock","checkpoint","sqlite-busy","locking"],"backgroundTag":"lock-acquire-failed","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}