Crash: SqliteException(261): while executing, database is locked, database is locked (code 261) #508

Closed
opened 2026-06-06 21:24:43 +00:00 by guettli · 1 comment
guettli commented 2026-06-06 21:24:43 +00:00 (Migrated from codeberg.org)

App Version: 0.1.1+1780774971
Build Mode: release
Git Commit: 65173d3
Platform: android S1RXS32.50-13-25
Dart: 3.12.0 (stable) (Fri May 8 01:51:14 2026 -0700) on "android_arm64"
Timestamp: 2026-06-06T21:24:22.569803Z

Error:

SqliteException(261): while executing, database is locked, database is locked (code 261)
  Causing statement: PRAGMA journal_mode = WAL;, parameters: 

Stack Trace:

package:sqlite3/src/implementation/exception.dart 101                     throwException
package:sqlite3/src/implementation/database.dart 299                      DatabaseImplementation.execute
package:sharedinbox/data/db/database.dart 805                             _openConnection.<fn>.<fn>
package:drift/src/sqlite3/database.dart 105                               Sqlite3Delegate._initializeDatabase
package:drift/src/sqlite3/database.dart 82                                Sqlite3Delegate.open
package:drift/src/runtime/executor/helpers/engines.dart 515               DelegatedDatabase.ensureOpen.<fn>
package:drift/src/utils/synchronized.dart 18                              Lock.synchronized.callBlockAndComplete.<fn>
package:drift/src/remote/server_impl.dart 156                             ServerImplementation._handleEnsureOpen
package:drift/src/remote/communication.dart 172                           DriftCommunication.setRequestHandler.<fn>
===== asynchronous gap ===========================
package:drift/src/remote/communication.dart 118                           DriftCommunication.request
package:drift/src/remote/client_impl.dart 188                             _RemoteQueryExecutor.ensureOpen
package:drift/src/utils/lazy_database.dart 78                             LazyDatabase.ensureOpen.<fn>
dart:async/zone_root.dart 48                                              _rootRunUnary
dart:async/zone.dart 733                                                  _CustomZone.runUnary
package:drift/src/runtime/api/connection_user.dart 170                    DatabaseConnectionUser.doWhenOpened.<fn>
package:drift/src/runtime/query_builder/statements/select/select.dart 90  SimpleSelectStatement._mapResponse
package:drift/src/runtime/query_builder/statements/query.dart 249         Selectable.getSingleOrNull
package:sharedinbox/main.dart 74                                          _registerPrefetchTaskFromStoredPrefs
package:sharedinbox/main.dart 53                                          main.<fn>

App Version: [0.1.1+1780774971](https://codeberg.org/guettli/sharedinbox/commit/65173d3) Build Mode: release Git Commit: [65173d3](https://codeberg.org/guettli/sharedinbox/commit/65173d3) Platform: android S1RXS32.50-13-25 Dart: 3.12.0 (stable) (Fri May 8 01:51:14 2026 -0700) on "android_arm64" Timestamp: 2026-06-06T21:24:22.569803Z Error: ``` SqliteException(261): while executing, database is locked, database is locked (code 261) Causing statement: PRAGMA journal_mode = WAL;, parameters: ``` Stack Trace: ``` package:sqlite3/src/implementation/exception.dart 101 throwException package:sqlite3/src/implementation/database.dart 299 DatabaseImplementation.execute package:sharedinbox/data/db/database.dart 805 _openConnection.<fn>.<fn> package:drift/src/sqlite3/database.dart 105 Sqlite3Delegate._initializeDatabase package:drift/src/sqlite3/database.dart 82 Sqlite3Delegate.open package:drift/src/runtime/executor/helpers/engines.dart 515 DelegatedDatabase.ensureOpen.<fn> package:drift/src/utils/synchronized.dart 18 Lock.synchronized.callBlockAndComplete.<fn> package:drift/src/remote/server_impl.dart 156 ServerImplementation._handleEnsureOpen package:drift/src/remote/communication.dart 172 DriftCommunication.setRequestHandler.<fn> ===== asynchronous gap =========================== package:drift/src/remote/communication.dart 118 DriftCommunication.request package:drift/src/remote/client_impl.dart 188 _RemoteQueryExecutor.ensureOpen package:drift/src/utils/lazy_database.dart 78 LazyDatabase.ensureOpen.<fn> dart:async/zone_root.dart 48 _rootRunUnary dart:async/zone.dart 733 _CustomZone.runUnary package:drift/src/runtime/api/connection_user.dart 170 DatabaseConnectionUser.doWhenOpened.<fn> package:drift/src/runtime/query_builder/statements/select/select.dart 90 SimpleSelectStatement._mapResponse package:drift/src/runtime/query_builder/statements/query.dart 249 Selectable.getSingleOrNull package:sharedinbox/main.dart 74 _registerPrefetchTaskFromStoredPrefs package:sharedinbox/main.dart 53 main.<fn> ```
guettlibot commented 2026-06-06 21:40:06 +00:00 (Migrated from codeberg.org)

The PR is open at https://codeberg.org/guettli/sharedinbox/pulls/510.


Here's a summary of what was done:

Root cause: When the Android app starts, a previously-scheduled WorkManager background task may already have the SQLite database open. The _openConnection() setup callback then tries to execute PRAGMA journal_mode = WAL; on a second connection, which fails with SqliteException(261)SQLITE_BUSY_SNAPSHOT. This bubbled up uncaught and crashed the app before the UI could render.

Fix (lib/data/db/database.dart):

  1. Moved PRAGMA busy_timeout = 5000 before the WAL pragma so SQLite's built-in retry handler covers plain SQLITE_BUSY (code 5) automatically.
  2. Extracted the setup logic into a named _setupPragmas function and wrapped the WAL pragma in a try/catch that swallows SqliteException when resultCode == 5. This covers both SQLITE_BUSY (5) and SQLITE_BUSY_SNAPSHOT (261, since 261 & 0xFF == 5). SQLITE_BUSY_SNAPSHOT only occurs on databases already in WAL mode, so the pragma is a no-op and it's safe to continue.
  3. Exposed setupPragmasForTesting (same pattern as the existing resolveDatabasePathForTesting) so tests can exercise the real production setup logic.

Test (test/unit/migration_test.dart): Added a regression test that opens a raw sqlite3 connection in WAL mode with an active read transaction, then calls setupPragmasForTesting on a second connection and asserts it does not throw.

The PR is open at https://codeberg.org/guettli/sharedinbox/pulls/510. --- Here's a summary of what was done: **Root cause:** When the Android app starts, a previously-scheduled WorkManager background task may already have the SQLite database open. The `_openConnection()` setup callback then tries to execute `PRAGMA journal_mode = WAL;` on a second connection, which fails with `SqliteException(261)` — `SQLITE_BUSY_SNAPSHOT`. This bubbled up uncaught and crashed the app before the UI could render. **Fix (`lib/data/db/database.dart`):** 1. Moved `PRAGMA busy_timeout = 5000` **before** the WAL pragma so SQLite's built-in retry handler covers plain `SQLITE_BUSY` (code 5) automatically. 2. Extracted the setup logic into a named `_setupPragmas` function and wrapped the WAL pragma in a `try/catch` that swallows `SqliteException` when `resultCode == 5`. This covers both `SQLITE_BUSY` (5) and `SQLITE_BUSY_SNAPSHOT` (261, since 261 & 0xFF == 5). `SQLITE_BUSY_SNAPSHOT` only occurs on databases already in WAL mode, so the pragma is a no-op and it's safe to continue. 3. Exposed `setupPragmasForTesting` (same pattern as the existing `resolveDatabasePathForTesting`) so tests can exercise the real production setup logic. **Test (`test/unit/migration_test.dart`):** Added a regression test that opens a raw sqlite3 connection in WAL mode with an active read transaction, then calls `setupPragmasForTesting` on a second connection and asserts it does not throw.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: guettli/sharedinbox#508