Skip to content

Commit 8a3152c

Browse files
committed
Make DatabaseLoginsStorage async
There hasn't been agreement on a grand vision for async Rust in app-services (#6549). I think part of the reason is that ADR is discussing too many things. This is my attempt to take a small change from that ADR and see if we agree. This change takes the async wrapping code that's currently in android-components and moves it to our app-services wrapper. This improves the API boundary IMO. The SYNC sync and the credentials management teams are responsible for threading issues. For example, we're the one's thinking through how to avoid blocking too many threads when we're waiting on the primary password dialog. Since we're responsible for threading issues, we should own the code On a practical level: I have some ideas on how to improve our threading strategy and it's simpler to think about those changes if all the code is in one repo. Here's the corresponding android-components change: bendk/firefox@d038ba6 If we agree on this, then I think we can do something similar for iOS. What do others think? Is this a good idea? Do we need/want an ADR for this?
1 parent 5358a19 commit 8a3152c

File tree

4 files changed

+49
-43
lines changed

4 files changed

+49
-43
lines changed

components/logins/android/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ dependencies {
1919
api project(':init_rust_components')
2020
api project(':sync15')
2121

22+
implementation libs.kotlin.coroutines
2223
implementation libs.mozilla.glean
2324
implementation project(':init_rust_components')
2425

2526
testImplementation libs.mozilla.glean.forUnitTests
27+
testImplementation libs.kotlin.coroutines.test
2628
testImplementation libs.androidx.test.core
2729
testImplementation libs.androidx.work.testing
2830
testImplementation project(":syncmanager")

components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,23 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
package mozilla.appservices.logins
6-
7-
/**
8-
* Import some private Glean types, so that we can use them in type declarations.
9-
*
10-
* I do not like importing these private classes, but I do like the nice generic
11-
* code they allow me to write! By agreement with the Glean team, we must not
12-
* instantiate anything from these classes, and it's on us to fix any bustage
13-
* on version updates.
14-
*/
6+
import kotlinx.coroutines.Dispatchers
7+
import kotlinx.coroutines.withContext
158
import mozilla.telemetry.glean.private.CounterMetricType
169
import mozilla.telemetry.glean.private.LabeledMetricType
10+
import kotlin.coroutines.CoroutineContext
1711
import org.mozilla.appservices.logins.GleanMetrics.LoginsStore as LoginsStoreMetrics
1812

1913
/**
2014
* An artifact of the uniffi conversion - a thin-ish wrapper around a
2115
* LoginStore.
2216
*/
2317

24-
class DatabaseLoginsStorage(dbPath: String, keyManager: KeyManager) : AutoCloseable {
18+
class DatabaseLoginsStorage(
19+
dbPath: String,
20+
keyManager: KeyManager,
21+
private val coroutineContext: CoroutineContext = Dispatchers.IO,
22+
) : AutoCloseable {
2523
private var store: LoginStore
2624

2725
init {
@@ -30,94 +28,94 @@ class DatabaseLoginsStorage(dbPath: String, keyManager: KeyManager) : AutoClosea
3028
}
3129

3230
@Throws(LoginsApiException::class)
33-
fun reset() {
34-
this.store.reset()
31+
suspend fun reset(): Unit = withContext(coroutineContext) {
32+
store.reset()
3533
}
3634

3735
@Throws(LoginsApiException::class)
38-
fun wipeLocal() {
39-
this.store.wipeLocal()
36+
suspend fun wipeLocal(): Unit = withContext(coroutineContext) {
37+
store.wipeLocal()
4038
}
4139

4240
@Throws(LoginsApiException::class)
43-
fun delete(id: String): Boolean {
44-
return writeQueryCounters.measure {
41+
suspend fun delete(id: String): Boolean = withContext(coroutineContext) {
42+
writeQueryCounters.measure {
4543
store.delete(id)
4644
}
4745
}
4846

4947
@Throws(LoginsApiException::class)
50-
fun get(id: String): Login? {
51-
return readQueryCounters.measure {
48+
suspend fun get(id: String): Login? = withContext(coroutineContext) {
49+
readQueryCounters.measure {
5250
store.get(id)
5351
}
5452
}
5553

5654
@Throws(LoginsApiException::class)
57-
fun touch(id: String) {
55+
suspend fun touch(id: String): Unit = withContext(coroutineContext) {
5856
writeQueryCounters.measure {
5957
store.touch(id)
6058
}
6159
}
6260

6361
@Throws(LoginsApiException::class)
64-
fun isEmpty(): Boolean {
65-
return readQueryCounters.measure {
62+
suspend fun isEmpty(): Boolean = withContext(coroutineContext) {
63+
readQueryCounters.measure {
6664
store.isEmpty()
6765
}
6866
}
6967

7068
@Throws(LoginsApiException::class)
71-
fun list(): List<Login> {
72-
return readQueryCounters.measure {
69+
suspend fun list(): List<Login> = withContext(coroutineContext) {
70+
readQueryCounters.measure {
7371
store.list()
7472
}
7573
}
7674

7775
@Throws(LoginsApiException::class)
78-
fun hasLoginsByBaseDomain(baseDomain: String): Boolean {
79-
return readQueryCounters.measure {
76+
suspend fun hasLoginsByBaseDomain(baseDomain: String): Boolean = withContext(coroutineContext) {
77+
readQueryCounters.measure {
8078
store.hasLoginsByBaseDomain(baseDomain)
8179
}
8280
}
8381

8482
@Throws(LoginsApiException::class)
85-
fun getByBaseDomain(baseDomain: String): List<Login> {
86-
return readQueryCounters.measure {
83+
suspend fun getByBaseDomain(baseDomain: String): List<Login> = withContext(coroutineContext) {
84+
readQueryCounters.measure {
8785
store.getByBaseDomain(baseDomain)
8886
}
8987
}
9088

9189
@Throws(LoginsApiException::class)
92-
fun findLoginToUpdate(look: LoginEntry): Login? {
93-
return readQueryCounters.measure {
90+
suspend fun findLoginToUpdate(look: LoginEntry): Login? = withContext(coroutineContext) {
91+
readQueryCounters.measure {
9492
store.findLoginToUpdate(look)
9593
}
9694
}
9795

9896
@Throws(LoginsApiException::class)
99-
fun add(entry: LoginEntry): Login {
100-
return writeQueryCounters.measure {
97+
suspend fun add(entry: LoginEntry): Login = withContext(coroutineContext) {
98+
writeQueryCounters.measure {
10199
store.add(entry)
102100
}
103101
}
104102

105103
@Throws(LoginsApiException::class)
106-
fun update(id: String, entry: LoginEntry): Login {
107-
return writeQueryCounters.measure {
104+
suspend fun update(id: String, entry: LoginEntry): Login = withContext(coroutineContext) {
105+
writeQueryCounters.measure {
108106
store.update(id, entry)
109107
}
110108
}
111109

112110
@Throws(LoginsApiException::class)
113-
fun addOrUpdate(entry: LoginEntry): Login {
114-
return writeQueryCounters.measure {
111+
suspend fun addOrUpdate(entry: LoginEntry): Login = withContext(coroutineContext) {
112+
writeQueryCounters.measure {
115113
store.addOrUpdate(entry)
116114
}
117115
}
118116

119117
fun registerWithSyncManager() {
120-
return store.registerWithSyncManager()
118+
store.registerWithSyncManager()
121119
}
122120

123121
@Synchronized

components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
package mozilla.appservices.logins
55

66
import androidx.test.core.app.ApplicationProvider
7+
import kotlinx.coroutines.test.runTest
78
import mozilla.appservices.RustComponentsInitializer
89
import mozilla.appservices.syncmanager.SyncManager
910
import mozilla.telemetry.glean.testing.GleanTestRule
1011
import org.junit.Assert.assertEquals
1112
import org.junit.Assert.assertFalse
1213
import org.junit.Assert.assertNotNull
1314
import org.junit.Assert.assertNull
14-
import org.junit.Assert.assertThrows
1515
import org.junit.Assert.assertTrue
1616
import org.junit.Assert.fail
1717
import org.junit.Rule
@@ -41,7 +41,7 @@ class DatabaseLoginsStorageTest {
4141
return DatabaseLoginsStorage(dbPath = dbPath.absolutePath, keyManager = keyManager)
4242
}
4343

44-
protected fun getTestStore(): DatabaseLoginsStorage {
44+
protected suspend fun getTestStore(): DatabaseLoginsStorage {
4545
val store = createTestStore()
4646

4747
store.add(
@@ -77,7 +77,7 @@ class DatabaseLoginsStorageTest {
7777
}
7878

7979
@Test
80-
fun testMetricsGathering() {
80+
fun testMetricsGathering() = runTest {
8181
val store = createTestStore()
8282

8383
assertNull(LoginsStoreMetrics.writeQueryCount.testGetValue())
@@ -132,7 +132,7 @@ class DatabaseLoginsStorageTest {
132132
}
133133

134134
@Test
135-
fun testTouch() {
135+
fun testTouch() = runTest {
136136
val store = getTestStore()
137137
val login = store.list()[0]
138138
// Wait 100ms so that touch is certain to change timeLastUsed.
@@ -145,13 +145,17 @@ class DatabaseLoginsStorageTest {
145145
assertEquals(login.timesUsed + 1, updatedLogin!!.timesUsed)
146146
assert(updatedLogin.timeLastUsed > login.timeLastUsed)
147147

148-
assertThrows(LoginsApiException.NoSuchRecord::class.java) { store.touch("abcdabcdabcd") }
148+
try {
149+
store.touch("abcdabcdabcd")
150+
} catch (e: LoginsApiException.NoSuchRecord) {
151+
// Expected error
152+
}
149153

150154
finishAndClose(store)
151155
}
152156

153157
@Test
154-
fun testDelete() {
158+
fun testDelete() = runTest {
155159
val store = getTestStore()
156160
val login = store.list()[0]
157161

@@ -165,7 +169,7 @@ class DatabaseLoginsStorageTest {
165169
}
166170

167171
@Test
168-
fun testWipeLocal() {
172+
fun testWipeLocal() = runTest {
169173
val test = getTestStore()
170174
val logins = test.list()
171175
assertEquals(2, logins.size)

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ protobuf-plugin = "0.9.5"
1414
# Kotlin
1515
kotlin-compiler = "2.1.21"
1616
kotlin-coroutines = "1.10.2"
17+
kotlin-coroutines-test = "1.10.2"
1718

1819
# Mozilla
1920
android-components = "139.0.20250417022706"
@@ -58,6 +59,7 @@ protobuf-compiler = { group = "com.google.protobuf", name = "protoc", version.re
5859
# Kotlin
5960
kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin-compiler" }
6061
kotlin-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlin-coroutines" }
62+
kotlin-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlin-coroutines-test" }
6163

6264
# Mozilla
6365
mozilla-concept-fetch = { group = "org.mozilla.components", name = "concept-fetch", version.ref = "android-components" }

0 commit comments

Comments
 (0)