Description
BucketStorage.select() in powersync_core calls _internalDb.execute() instead of _internalDb.getAll(). The comment says "Use only for read statements", but execute() routes through SqliteQueries.execute() which acquires a write lock (writeLock()), while getAll() only acquires a read lock (readLock()).
File: packages/powersync_core/lib/src/sync/bucket_storage.dart, lines 30-34
// Use only for read statements
Future<ResultSet> select(String query,
[List<Object?> parameters = const []]) async {
return await _internalDb.execute(query, parameters); // ← writeLock
}
Fix:
return await _internalDb.getAll(query, parameters); // ← readLock
Impact
On SyncSqliteConnection, both readLock() and writeLock() use the same shared cross-isolate mutex, so this change alone doesn't fix the contention there. However, on the SqliteConnectionPool (used when BucketStorage wraps the main database on web), execute() takes the global write mutex while getAll() uses per-connection read mutexes — so this is a real concurrency bug on web and in tests.
Context
We discovered this while investigating a ~3-minute read blockage on the congratulation screen of our Flutter app. After a learning session, all Drift .watch() queries and one-shot reads are blocked while PowerSync uploads CRUD data and processes the sync stream. The sync isolate holds the shared mutex for extended periods, which blocks all main-isolate reads.
The BucketStorage.select() issue is one contributing factor. The larger issue appears to be that SyncSqliteConnection uses a single shared mutex for both reads and writes, and this mutex is shared cross-isolate with the main database's write connection. During sync processing (both upload coordination and download data ingestion), this mutex is held for long periods, effectively serializing all database access.
Environment
- powersync: 1.18.0
- powersync_core: 1.8.0
- sqlite_async: 0.13.1
- Flutter 3.41
- Platform: macOS (also affects iOS/Android)
Description
BucketStorage.select()inpowersync_corecalls_internalDb.execute()instead of_internalDb.getAll(). The comment says "Use only for read statements", butexecute()routes throughSqliteQueries.execute()which acquires a write lock (writeLock()), whilegetAll()only acquires a read lock (readLock()).File:
packages/powersync_core/lib/src/sync/bucket_storage.dart, lines 30-34Fix:
Impact
On
SyncSqliteConnection, bothreadLock()andwriteLock()use the same shared cross-isolate mutex, so this change alone doesn't fix the contention there. However, on theSqliteConnectionPool(used whenBucketStoragewraps the main database on web),execute()takes the global write mutex whilegetAll()uses per-connection read mutexes — so this is a real concurrency bug on web and in tests.Context
We discovered this while investigating a ~3-minute read blockage on the congratulation screen of our Flutter app. After a learning session, all Drift
.watch()queries and one-shot reads are blocked while PowerSync uploads CRUD data and processes the sync stream. The sync isolate holds the shared mutex for extended periods, which blocks all main-isolate reads.The
BucketStorage.select()issue is one contributing factor. The larger issue appears to be thatSyncSqliteConnectionuses a single shared mutex for both reads and writes, and this mutex is shared cross-isolate with the main database's write connection. During sync processing (both upload coordination and download data ingestion), this mutex is held for long periods, effectively serializing all database access.Environment