Skip to content

Commit 394507b

Browse files
Merge remote-tracking branch 'origin/main' into concurrent
2 parents f9a2218 + 7d62a23 commit 394507b

File tree

10 files changed

+252
-107
lines changed

10 files changed

+252
-107
lines changed

core-tests-android/src/androidTest/java/com/powersync/AndroidDatabaseTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ class AndroidDatabaseTest {
3434

3535
@After
3636
fun tearDown() {
37-
runBlocking { database.disconnectAndClear(true) }
37+
runBlocking {
38+
database.disconnectAndClear(true)
39+
database.close()
40+
}
3841
}
3942

4043
@Test

core/build.gradle.kts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,8 @@ kotlin {
217217
dependsOn(commonTest.get())
218218
}
219219

220-
val commonJDBC by creating {
221-
kotlin.srcDir("commonJDBC")
220+
val commonJava by creating {
221+
kotlin.srcDir("commonJava")
222222
dependsOn(commonMain.get())
223223
dependencies {
224224
implementation(libs.sqlite.jdbc)
@@ -240,14 +240,18 @@ kotlin {
240240
api(libs.kermit)
241241
}
242242

243-
androidMain.dependencies {
244-
implementation(libs.ktor.client.okhttp)
245-
implementation(libs.sqlite.jdbc)
243+
androidMain {
244+
dependsOn(commonJava)
245+
dependencies.implementation(libs.ktor.client.okhttp)
246246
}
247247

248-
jvmMain.dependencies {
249-
implementation(libs.ktor.client.okhttp)
250-
implementation(libs.sqlite.jdbc)
248+
jvmMain {
249+
dependsOn(commonJava)
250+
251+
dependencies {
252+
implementation(libs.ktor.client.okhttp)
253+
implementation(libs.sqlite.jdbc)
254+
}
251255
}
252256

253257
iosMain.dependencies {

core/src/commonIntegrationTest/kotlin/com/powersync/DatabaseTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import co.touchlab.kermit.Logger
66
import co.touchlab.kermit.Severity
77
import co.touchlab.kermit.TestConfig
88
import co.touchlab.kermit.TestLogWriter
9-
import com.powersync.db.PowerSyncDatabaseImpl
9+
import com.powersync.db.ActiveDatabaseGroup
1010
import com.powersync.db.schema.Schema
1111
import com.powersync.testutils.UserRow
1212
import com.powersync.testutils.waitFor
@@ -228,7 +228,7 @@ class DatabaseTest {
228228
waitFor {
229229
assertNotNull(
230230
logWriter.logs.find {
231-
it.message == PowerSyncDatabaseImpl.multipleInstancesMessage
231+
it.message == ActiveDatabaseGroup.multipleInstancesMessage
232232
},
233233
)
234234
}

core/src/commonIntegrationTest/kotlin/com/powersync/SyncIntegrationTest.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class SyncIntegrationTest {
120120
fun testPartialSync() =
121121
runTest {
122122
val syncStream = syncStream()
123-
database.connect(syncStream, 1000L)
123+
database.connectInternal(syncStream, 1000L)
124124

125125
val checksums =
126126
buildList {
@@ -214,7 +214,7 @@ class SyncIntegrationTest {
214214
fun testRemembersLastPartialSync() =
215215
runTest {
216216
val syncStream = syncStream()
217-
database.connect(syncStream, 1000L)
217+
database.connectInternal(syncStream, 1000L)
218218

219219
syncLines.send(
220220
SyncLine.FullCheckpoint(
@@ -253,7 +253,7 @@ class SyncIntegrationTest {
253253
fun setsDownloadingState() =
254254
runTest {
255255
val syncStream = syncStream()
256-
database.connect(syncStream, 1000L)
256+
database.connectInternal(syncStream, 1000L)
257257

258258
turbineScope(timeout = 10.0.seconds) {
259259
val turbine = database.currentStatus.asFlow().testIn(this)
@@ -291,7 +291,7 @@ class SyncIntegrationTest {
291291
val syncStream = syncStream()
292292
val turbine = database.currentStatus.asFlow().testIn(this)
293293

294-
database.connect(syncStream, 1000L)
294+
database.connectInternal(syncStream, 1000L)
295295
turbine.waitFor { it.connecting }
296296

297297
database.disconnect()
@@ -308,7 +308,7 @@ class SyncIntegrationTest {
308308
fun testMultipleSyncsDoNotCreateMultipleStatusEntries() =
309309
runTest {
310310
val syncStream = syncStream()
311-
database.connect(syncStream, 1000L)
311+
database.connectInternal(syncStream, 1000L)
312312

313313
turbineScope(timeout = 10.0.seconds) {
314314
val turbine = database.currentStatus.asFlow().testIn(this)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.powersync.db
2+
3+
internal actual fun disposeWhenDeallocated(resource: ActiveDatabaseResource): Any {
4+
// We can't do this on Java 8 :(
5+
return object {}
6+
}
7+
8+
// This would require Java 9+
9+
10+
/*
11+
import java.lang.ref.Cleaner
12+
13+
internal actual fun disposeWhenDeallocated(resource: ActiveDatabaseResource): Any {
14+
// Note: It's important that the returned object does not reference the resource directly
15+
val wrapper = CleanableWrapper()
16+
CleanableWrapper.cleaner.register(wrapper, resource::dispose)
17+
return wrapper
18+
}
19+
20+
private class CleanableWrapper {
21+
var cleanable: Cleaner.Cleanable? = null
22+
23+
companion object {
24+
val cleaner: Cleaner = Cleaner.create()
25+
}
26+
}
27+
*/
Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,94 @@
11
package com.powersync.db
22

3-
import com.powersync.PowerSyncDatabase
4-
import com.powersync.utils.ExclusiveMethodProvider
5-
6-
internal class ActiveInstanceStore : ExclusiveMethodProvider() {
7-
private val instances = mutableListOf<PowerSyncDatabase>()
8-
9-
/**
10-
* Registers an instance. Returns true if multiple instances with the same identifier are
11-
* present.
12-
*/
13-
suspend fun registerAndCheckInstance(db: PowerSyncDatabase) =
14-
exclusiveMethod("instances") {
15-
instances.add(db)
16-
return@exclusiveMethod instances.filter { it.identifier == db.identifier }.size > 1
3+
import co.touchlab.kermit.Logger
4+
import co.touchlab.stately.concurrency.AtomicBoolean
5+
import co.touchlab.stately.concurrency.Synchronizable
6+
import co.touchlab.stately.concurrency.synchronize
7+
import kotlinx.coroutines.sync.Mutex
8+
9+
/**
10+
* Returns an object that, when deallocated, calls [ActiveDatabaseResource.dispose].
11+
*/
12+
internal expect fun disposeWhenDeallocated(resource: ActiveDatabaseResource): Any
13+
14+
/**
15+
* An collection of PowerSync databases with the same path / identifier.
16+
*
17+
* We expect that each group will only ever have one database because we encourage users to write their databases as
18+
* singletons. We print a warning when two databases are part of the same group.
19+
* Additionally, we want to avoid two databases in the same group having a sync stream open at the same time to avoid
20+
* duplicate resources being used. For this reason, each active database group has a coroutine mutex guarding the
21+
* sync job.
22+
*/
23+
internal class ActiveDatabaseGroup(
24+
val identifier: String,
25+
private val collection: GroupsCollection,
26+
) {
27+
internal var refCount = 0 // Guarded by companion object
28+
internal val syncMutex = Mutex()
29+
30+
fun removeUsage() {
31+
collection.synchronize {
32+
if (--refCount == 0) {
33+
collection.allGroups.remove(this)
34+
}
1735
}
36+
}
37+
38+
internal open class GroupsCollection : Synchronizable() {
39+
internal val allGroups = mutableListOf<ActiveDatabaseGroup>()
40+
41+
private fun findGroup(
42+
warnOnDuplicate: Logger,
43+
identifier: String,
44+
): ActiveDatabaseGroup =
45+
synchronize {
46+
val existing = allGroups.asSequence().firstOrNull { it.identifier == identifier }
47+
val resolvedGroup =
48+
if (existing == null) {
49+
val added = ActiveDatabaseGroup(identifier, this)
50+
allGroups.add(added)
51+
added
52+
} else {
53+
existing
54+
}
55+
56+
if (resolvedGroup.refCount++ != 0) {
57+
warnOnDuplicate.w { multipleInstancesMessage }
58+
}
59+
60+
resolvedGroup
61+
}
62+
63+
internal fun referenceDatabase(
64+
warnOnDuplicate: Logger,
65+
identifier: String,
66+
): Pair<ActiveDatabaseResource, Any> {
67+
val group = findGroup(warnOnDuplicate, identifier)
68+
val resource = ActiveDatabaseResource(group)
69+
70+
return resource to disposeWhenDeallocated(resource)
71+
}
72+
}
73+
74+
companion object : GroupsCollection() {
75+
internal val multipleInstancesMessage =
76+
"""
77+
Multiple PowerSync instances for the same database have been detected.
78+
This can cause unexpected results.
79+
Please check your PowerSync client instantiation logic if this is not intentional.
80+
""".trimIndent()
81+
}
82+
}
83+
84+
internal class ActiveDatabaseResource(
85+
val group: ActiveDatabaseGroup,
86+
) {
87+
val disposed = AtomicBoolean(false)
1888

19-
suspend fun removeInstance(db: PowerSyncDatabase) =
20-
exclusiveMethod("instances") {
21-
instances.remove(db)
89+
fun dispose() {
90+
if (disposed.compareAndSet(false, true)) {
91+
group.removeUsage()
2292
}
93+
}
2394
}

0 commit comments

Comments
 (0)