Skip to content

Commit 12d6188

Browse files
authored
Merge pull request #266 from powersync-ja/warn-on-duplicate-database
Warn on duplicate database instances
2 parents 6168592 + 762c8ca commit 12d6188

11 files changed

+116
-20
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'package:meta/meta.dart';
2+
import 'package:sqlite_async/sqlite_async.dart';
3+
4+
/// A collection of PowerSync database instances that are using the same
5+
/// underlying SQLite database.
6+
///
7+
/// We expect that each group will only ever have one database because we
8+
/// encourage users to manage their databases as singletons. So, we print a
9+
/// warning when two databases are part of the same group.
10+
///
11+
/// This can only detect two database instances being opened on the same
12+
/// isolate, we can't provide these checks acros isolates. Since most users
13+
/// aren't opening databases on background isolates though, this still guards
14+
/// against most misuses.
15+
@internal
16+
final class ActiveDatabaseGroup {
17+
int refCount = 0;
18+
19+
/// Use to prevent multiple connections from being opened concurrently
20+
final Mutex syncConnectMutex = Mutex();
21+
final String identifier;
22+
23+
ActiveDatabaseGroup._(this.identifier);
24+
25+
void close() {
26+
if (--refCount == 0) {
27+
final removedGroup = _activeGroups.remove(identifier);
28+
assert(removedGroup == this);
29+
}
30+
}
31+
32+
static final Map<String, ActiveDatabaseGroup> _activeGroups = {};
33+
34+
static ActiveDatabaseGroup referenceDatabase(String identifier) {
35+
final group = _activeGroups.putIfAbsent(
36+
identifier, () => ActiveDatabaseGroup._(identifier));
37+
group.refCount++;
38+
return group;
39+
}
40+
}

packages/powersync_core/lib/src/database/powersync_database.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,6 @@ abstract class PowerSyncDatabase
7373
required SqliteDatabase database,
7474
Logger? loggers}) {
7575
return PowerSyncDatabaseImpl.withDatabase(
76-
schema: schema, database: database);
76+
schema: schema, database: database, logger: loggers);
7777
}
7878
}

packages/powersync_core/lib/src/database/powersync_database_impl_stub.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class PowerSyncDatabaseImpl
8080
factory PowerSyncDatabaseImpl.withDatabase(
8181
{required Schema schema,
8282
required SqliteDatabase database,
83-
Logger? loggers}) {
83+
Logger? logger}) {
8484
throw UnimplementedError();
8585
}
8686

packages/powersync_core/lib/src/database/powersync_db_mixin.dart

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:powersync_core/sqlite_async.dart';
77
import 'package:powersync_core/src/abort_controller.dart';
88
import 'package:powersync_core/src/connector.dart';
99
import 'package:powersync_core/src/crud.dart';
10+
import 'package:powersync_core/src/database/active_instances.dart';
1011
import 'package:powersync_core/src/database/core_version.dart';
1112
import 'package:powersync_core/src/powersync_update_notification.dart';
1213
import 'package:powersync_core/src/schema.dart';
@@ -45,8 +46,7 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
4546
StreamController<SyncStatus> statusStreamController =
4647
StreamController<SyncStatus>.broadcast();
4748

48-
/// Use to prevent multiple connections from being opened concurrently
49-
final Mutex _connectMutex = Mutex();
49+
late final ActiveDatabaseGroup _activeGroup;
5050

5151
@override
5252

@@ -71,6 +71,22 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
7171

7272
@protected
7373
Future<void> baseInit() async {
74+
String identifier = 'memory';
75+
try {
76+
identifier = database.openFactory.path;
77+
} catch (ignore) {
78+
// The in-memory database used in some tests doesn't have an open factory.
79+
}
80+
81+
_activeGroup = ActiveDatabaseGroup.referenceDatabase(identifier);
82+
if (_activeGroup.refCount > 1) {
83+
logger.warning(
84+
'Multiple instances for the same database have been detected. '
85+
'This can cause unexpected results, please check your PowerSync client '
86+
'instantiation logic if this is not intentional',
87+
);
88+
}
89+
7490
statusStream = statusStreamController.stream;
7591
updates = powerSyncUpdateNotifications(database.updates);
7692

@@ -209,12 +225,16 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
209225
await isInitialized;
210226
// Disconnect any active sync connection.
211227
await disconnect();
212-
// Now we can close the database
213-
await database.close();
214228

215-
// If there are paused subscriptionso n the status stream, don't delay
216-
// closing the database because of that.
217-
unawaited(statusStreamController.close());
229+
if (!database.closed) {
230+
// Now we can close the database
231+
await database.close();
232+
233+
// If there are paused subscriptionso n the status stream, don't delay
234+
// closing the database because of that.
235+
unawaited(statusStreamController.close());
236+
_activeGroup.close();
237+
}
218238
}
219239

220240
/// Connect to the PowerSync service, and keep the databases in sync.
@@ -233,7 +253,7 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
233253
Zone current = Zone.current;
234254

235255
Future<void> reconnect() {
236-
return _connectMutex.lock(() => baseConnect(
256+
return _activeGroup.syncConnectMutex.lock(() => baseConnect(
237257
connector: connector,
238258
crudThrottleTime: crudThrottleTime,
239259
// The reconnect function needs to run in the original zone,

packages/powersync_core/lib/src/database/web/web_powersync_database.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ class PowerSyncDatabaseImpl
3737
@override
3838
SqliteDatabase database;
3939

40-
late final DefaultSqliteOpenFactory openFactory;
41-
4240
@override
4341
@protected
4442
late Future<void> isInitialized;
@@ -95,8 +93,7 @@ class PowerSyncDatabaseImpl
9593
Logger? logger}) {
9694
final db = SqliteDatabase.withFactory(openFactory, maxReaders: 1);
9795
return PowerSyncDatabaseImpl.withDatabase(
98-
schema: schema, logger: logger, database: db)
99-
..openFactory = openFactory;
96+
schema: schema, logger: logger, database: db);
10097
}
10198

10299
/// Open a PowerSyncDatabase on an existing [SqliteDatabase].

packages/powersync_core/lib/src/web/sync_controller.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,6 @@ class SyncWorkerHandle implements StreamingSync {
115115
@override
116116
Future<void> streamingSync() async {
117117
await _channel.startSynchronization(
118-
database.openFactory.path, crudThrottleTimeMs, syncParams);
118+
database.database.openFactory.path, crudThrottleTimeMs, syncParams);
119119
}
120120
}

packages/powersync_core/test/in_memory_sync_test.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import 'utils/in_memory_http.dart';
1515
import 'utils/test_utils_impl.dart';
1616

1717
void main() {
18+
final ignoredLogger = Logger.detached('powersync.test')..level = Level.OFF;
19+
1820
group('in-memory sync tests', () {
1921
late final testUtils = TestUtils();
2022

@@ -100,7 +102,8 @@ void main() {
100102
await expectLater(
101103
status, emits(isSyncStatus(downloading: false, hasSynced: true)));
102104

103-
final independentDb = factory.wrapRaw(raw);
105+
final independentDb = factory.wrapRaw(raw, logger: ignoredLogger);
106+
addTearDown(independentDb.close);
104107
// Even though this database doesn't have a sync client attached to it,
105108
// is should reconstruct hasSynced from the database.
106109
await independentDb.initialize();
@@ -272,7 +275,8 @@ void main() {
272275
await database.waitForFirstSync(priority: BucketPriority(1));
273276
expect(database.currentStatus.hasSynced, isFalse);
274277

275-
final independentDb = factory.wrapRaw(raw);
278+
final independentDb = factory.wrapRaw(raw, logger: ignoredLogger);
279+
addTearDown(independentDb.close);
276280
await independentDb.initialize();
277281
expect(independentDb.currentStatus.hasSynced, isFalse);
278282
// Completing a sync for prio 1 implies a completed sync for prio 0

packages/powersync_core/test/powersync_shared_test.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:logging/logging.dart';
12
import 'package:powersync_core/powersync_core.dart';
23
import 'package:sqlite_async/mutex.dart';
34
import 'package:test/test.dart';
@@ -20,6 +21,33 @@ void main() {
2021
await testUtils.cleanDb(path: path);
2122
});
2223

24+
test('warns on duplicate database', () async {
25+
final logger = Logger.detached('powersync.test')..level = Level.WARNING;
26+
final events = <LogRecord>[];
27+
final subscription = logger.onRecord.listen(events.add);
28+
addTearDown(subscription.cancel);
29+
30+
final firstInstance =
31+
await testUtils.setupPowerSync(path: path, logger: logger);
32+
await firstInstance.initialize();
33+
expect(events, isEmpty);
34+
35+
final secondInstance =
36+
await testUtils.setupPowerSync(path: path, logger: logger);
37+
await secondInstance.initialize();
38+
expect(
39+
events,
40+
contains(
41+
isA<LogRecord>().having(
42+
(e) => e.message,
43+
'message',
44+
contains(
45+
'Multiple instances for the same database have been detected.'),
46+
),
47+
),
48+
);
49+
});
50+
2351
test('should not allow direct db calls within a transaction callback',
2452
() async {
2553
final db = await testUtils.setupPowerSync(path: path);

packages/powersync_core/test/utils/abstract_test_utils.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:powersync_core/src/bucket_storage.dart';
55
import 'package:powersync_core/src/streaming_sync.dart';
66
import 'package:sqlite_async/sqlite3_common.dart';
77
import 'package:sqlite_async/sqlite_async.dart';
8+
import 'package:test/test.dart';
89
import 'package:test_api/src/backend/invoker.dart';
910

1011
const schema = Schema([
@@ -63,11 +64,15 @@ abstract mixin class TestPowerSyncFactory implements PowerSyncOpenFactory {
6364
return (raw, wrapRaw(raw));
6465
}
6566

66-
PowerSyncDatabase wrapRaw(CommonDatabase raw) {
67+
PowerSyncDatabase wrapRaw(
68+
CommonDatabase raw, {
69+
Logger? logger,
70+
}) {
6771
return PowerSyncDatabase.withDatabase(
6872
schema: schema,
6973
database: SqliteDatabase.singleConnection(
7074
SqliteConnection.synchronousWrapper(raw)),
75+
loggers: logger,
7176
);
7277
}
7378
}
@@ -95,6 +100,7 @@ abstract class AbstractTestUtils {
95100
schema: schema ?? defaultSchema,
96101
logger: logger ?? _makeTestLogger(name: _testName));
97102
await db.initialize();
103+
addTearDown(db.close);
98104
return db;
99105
}
100106

packages/powersync_core/test/utils/native_test_utils.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ class TestOpenFactory extends PowerSyncOpenFactory with TestPowerSyncFactory {
1919
return DynamicLibrary.open('libsqlite3.so.0');
2020
});
2121
sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.macOS, () {
22-
return DynamicLibrary.open('libsqlite3.dylib');
22+
return DynamicLibrary.open(
23+
'/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib');
2324
});
2425
}
2526

packages/powersync_core/test/utils/web_test_utils.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class TestUtils extends AbstractTestUtils {
7878
Future<PowerSyncDatabase> setupPowerSync(
7979
{String? path, Schema? schema, Logger? logger}) async {
8080
await _isInitialized;
81-
return super.setupPowerSync(path: path, schema: schema);
81+
return super.setupPowerSync(path: path, schema: schema, logger: logger);
8282
}
8383

8484
@override

0 commit comments

Comments
 (0)