diff --git a/packages/powersync/lib/src/open_factory.dart b/packages/powersync/lib/src/open_factory.dart index 7517eb4e..a9ea79fd 100644 --- a/packages/powersync/lib/src/open_factory.dart +++ b/packages/powersync/lib/src/open_factory.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; +import 'dart:math'; import 'package:sqlite_async/sqlite3.dart' as sqlite; import 'package:sqlite_async/sqlite_async.dart'; @@ -39,12 +40,41 @@ class PowerSyncOpenFactory extends DefaultSqliteOpenFactory { sqlite.Database open(SqliteOpenOptions options) { // ignore: deprecated_member_use_from_same_package _sqliteSetup?.setup(); - final db = super.open(options); + final db = _retriedOpen(options); db.execute('PRAGMA recursive_triggers = TRUE'); setupFunctions(db); return db; } + /// When opening the powersync connection and the standard write connection + /// at the same time, one could fail with this error: + /// + /// SqliteException(5): while opening the database, automatic extension loading failed: , database is locked (code 5) + /// + /// It happens before we have a chance to set the busy timeout, so we just + /// retry opening the database. + /// + /// Usually a delay of 1-2ms is sufficient for the next try to succeed, but + /// we increase the retry delay up to 16ms per retry, and a maximum of 500ms + /// in total. + sqlite.Database _retriedOpen(SqliteOpenOptions options) { + final stopwatch = Stopwatch()..start(); + var retryDelay = 2; + while (stopwatch.elapsedMilliseconds < 500) { + try { + return super.open(options); + } catch (e) { + if (e is sqlite.SqliteException && e.resultCode == 5) { + sleep(Duration(milliseconds: retryDelay)); + retryDelay = min(retryDelay * 2, 16); + continue; + } + rethrow; + } + } + throw AssertionError('Cannot reach this point'); + } + void setupFunctions(sqlite.Database db) { db.createFunction( functionName: 'uuid', diff --git a/packages/powersync/test/bucket_storage_test.dart b/packages/powersync/test/bucket_storage_test.dart index aad21265..1b3cc159 100644 --- a/packages/powersync/test/bucket_storage_test.dart +++ b/packages/powersync/test/bucket_storage_test.dart @@ -675,7 +675,7 @@ void main() { ])); }); - test('should revert a failing update', () async { + test('should revert a failing insert', () async { await bucketStorage.saveSyncData(SyncDataBatch([ SyncBucketData( bucket: 'bucket1', @@ -688,7 +688,7 @@ void main() { writeCheckpoint: '3', checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); - // Local save + // Local insert, later rejected by server db.execute('INSERT INTO assets(id, description) VALUES(?, ?)', ['O3', 'inserted']); final batch = bucketStorage.getCrudBatch(); @@ -725,7 +725,7 @@ void main() { writeCheckpoint: '3', checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); - // Local save + // Local delete, later rejected by server db.execute('DELETE FROM assets WHERE id = ?', ['O2']); expect(db.select('SELECT description FROM assets WHERE id = \'O2\''), @@ -750,7 +750,7 @@ void main() { ])); }); - test('should revert a failing insert', () async { + test('should revert a failing update', () async { await bucketStorage.saveSyncData(SyncDataBatch([ SyncBucketData( bucket: 'bucket1', @@ -763,11 +763,15 @@ void main() { writeCheckpoint: '3', checksums: [BucketChecksum(bucket: 'bucket1', checksum: 6)])); - // Local save - db.execute('DELETE FROM assets WHERE id = ?', ['O2']); + // Local update, later rejected by server + db.execute( + 'UPDATE assets SET description = ? WHERE id = ?', ['updated', 'O2']); - expect(db.select('SELECT description FROM assets WHERE id = \'O2\''), - equals([])); + expect( + db.select('SELECT description FROM assets WHERE id = \'O2\''), + equals([ + {'description': 'updated'} + ])); // Simulate a permissions error when uploading - data should be preserved. final batch = bucketStorage.getCrudBatch(); await batch!.complete(); diff --git a/packages/powersync/test/offline_online_test.dart b/packages/powersync/test/offline_online_test.dart index af497a5b..653ac12c 100644 --- a/packages/powersync/test/offline_online_test.dart +++ b/packages/powersync/test/offline_online_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:powersync/powersync.dart'; import 'package:test/test.dart'; @@ -113,16 +115,27 @@ void main() { await tx.execute('DELETE FROM local_assets'); }); + final crud = (await db.getAll('SELECT data FROM ps_crud ORDER BY id')) + .map((d) => jsonDecode(d['data'])) + .toList(); expect( - await db.getAll('SELECT data FROM ps_crud ORDER BY id'), + crud, equals([ { - 'data': - '{"op":"PUT","type":"customers","id":"$customerId","data":{"name":"test customer","email":"test@example.org"}}' + "op": "PUT", + "type": "customers", + "id": customerId, + "data": {"email": "test@example.org", "name": "test customer"} }, { - 'data': - '{"op":"PUT","type":"assets","id":"$assetId","data":{"user_id":"$userId","customer_id":"$customerId","description":"test."}}' + "op": "PUT", + "type": "assets", + "id": assetId, + "data": { + "user_id": userId, + "customer_id": customerId, + "description": "test." + } } ])); });