Skip to content

Commit 84f2a97

Browse files
authored
Stop throwing exceptions when DatabaseImpl encounters an old version (#4841)
* Stop throwing exceptions when DatabaseImpl encounters an old version insertOrUpdate now returns a Boolean instead of a version number Int, which indicates whether the operation was applied or not. Previously we would throw an exception if the version number given in the request was the same or older than the one stored in the database. Now, getting an old version number is a no-op, and will return false. * Fixes for failing tests * lint
1 parent 4d90a8d commit 84f2a97

File tree

7 files changed

+71
-67
lines changed

7 files changed

+71
-67
lines changed

java/arcs/android/storage/database/DatabaseImpl.kt

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ class DatabaseImpl(
351351
storageKey: StorageKey,
352352
data: DatabaseData,
353353
originatingClientId: Int?
354-
): Int = stats.insertUpdate.timeSuspending { counters ->
354+
): Boolean = stats.insertUpdate.timeSuspending { counters ->
355355
when (data) {
356356
is DatabaseData.Entity -> {
357357
counters.increment(DatabaseCounters.INSERTUPDATE_ENTITY)
@@ -366,20 +366,20 @@ class DatabaseImpl(
366366
insertOrUpdate(storageKey, data, counters)
367367
}
368368
}
369-
// TODO: Return a proper database version number.
370-
return@timeSuspending data.databaseVersion
371-
}.also { newVersion ->
372-
clientFlow.filter { it.storageKey == storageKey }
373-
.onEach { it.onDatabaseUpdate(data, newVersion, originatingClientId) }
374-
.launchIn(CoroutineScope(coroutineContext))
369+
}.also { success ->
370+
if (success) {
371+
clientFlow.filter { it.storageKey == storageKey }
372+
.onEach { it.onDatabaseUpdate(data, data.databaseVersion, originatingClientId) }
373+
.launchIn(CoroutineScope(coroutineContext))
374+
}
375375
}
376376

377377
@VisibleForTesting
378378
suspend fun insertOrUpdate(
379379
storageKey: StorageKey,
380380
data: DatabaseData.Entity,
381381
counters: Counters? = null
382-
) = writableDatabase.useTransaction {
382+
): Boolean = writableDatabase.useTransaction {
383383
val db = this
384384
val entity = data.rawEntity
385385
// Fetch/create the entity's type ID.
@@ -395,7 +395,8 @@ class DatabaseImpl(
395395
data.databaseVersion,
396396
db,
397397
counters
398-
)
398+
) ?: return@useTransaction false // Database has newer data. Don't apply the given op.
399+
399400
// Insert the entity's field types.
400401
counters?.increment(DatabaseCounters.GET_ENTITY_FIELDS)
401402
val fields = getSchemaFields(schemaTypeId, db)
@@ -452,6 +453,7 @@ class DatabaseImpl(
452453
SQLiteDatabase.CONFLICT_REPLACE
453454
)
454455
}
456+
true
455457
}
456458

457459
/**
@@ -505,7 +507,7 @@ class DatabaseImpl(
505507
data: DatabaseData.Collection,
506508
dataType: DataType,
507509
counters: Counters?
508-
) = writableDatabase.useTransaction {
510+
): Boolean = writableDatabase.useTransaction {
509511
val db = this
510512

511513
// Fetch/create the entity's type ID.
@@ -560,7 +562,9 @@ class DatabaseImpl(
560562
} else {
561563
// Collection already exists; delete all existing entries.
562564
val collectionId = metadata.collectionId
563-
if (data.databaseVersion <= metadata.versionNumber) return@useTransaction
565+
if (data.databaseVersion != metadata.versionNumber + 1) {
566+
return@useTransaction false
567+
}
564568

565569
// TODO: Don't blindly delete everything and re-insert: only insert/remove the diff.
566570
when (dataType) {
@@ -612,14 +616,15 @@ class DatabaseImpl(
612616
content.apply { put("value_id", referenceId) }
613617
)
614618
}
619+
true
615620
}
616621

617622
@VisibleForTesting
618623
suspend fun insertOrUpdate(
619624
storageKey: StorageKey,
620625
data: DatabaseData.Singleton,
621626
counters: Counters? = null
622-
) {
627+
): Boolean {
623628
// Convert Singleton into a a zero-or-one-element Collection.
624629
val set = mutableSetOf<Reference>()
625630
data.reference?.let { set.add(it) }
@@ -632,7 +637,7 @@ class DatabaseImpl(
632637
)
633638
}
634639
// Store the Collection as a Singleton.
635-
insertOrUpdate(storageKey, collectionData, DataType.Singleton, counters)
640+
return insertOrUpdate(storageKey, collectionData, DataType.Singleton, counters)
636641
}
637642

638643
override suspend fun delete(
@@ -765,6 +770,9 @@ class DatabaseImpl(
765770
/**
766771
* Creates a new storage key ID for the given entity [StorageKey]. If one already exists, it and
767772
* all data for the existing entity are deleted first before creating a new one.
773+
*
774+
* @returns the new storage key ID for the entity if one was successfully created, or null
775+
* otherwise (e.g. if the given version number was too low)
768776
*/
769777
@VisibleForTesting
770778
suspend fun createEntityStorageKeyId(
@@ -777,7 +785,7 @@ class DatabaseImpl(
777785
databaseVersion: Int,
778786
db: SQLiteDatabase,
779787
counters: Counters? = null
780-
): StorageKeyId = db.transaction {
788+
): StorageKeyId? = db.transaction {
781789
// TODO: Use an LRU cache.
782790
counters?.increment(DatabaseCounters.GET_ENTITY_STORAGEKEY_ID)
783791
rawQuery(
@@ -803,9 +811,8 @@ class DatabaseImpl(
803811
"$storedEntityId."
804812
}
805813
val storedVersion = it.getInt(2)
806-
require(databaseVersion > storedVersion) {
807-
"Given version ($databaseVersion) must be greater than version in database " +
808-
"($storedVersion) when updating storage key $storageKey."
814+
if (databaseVersion != storedVersion + 1) {
815+
return@transaction null
809816
}
810817

811818
// Remove the existing entity.
@@ -1220,6 +1227,7 @@ class DatabaseImpl(
12201227
val isCollection: Boolean
12211228
)
12221229

1230+
@VisibleForTesting
12231231
data class CollectionMetadata(
12241232
val collectionId: CollectionId,
12251233
val versionMap: VersionMap,

java/arcs/core/storage/database/Database.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ interface Database {
3434
storageKey: StorageKey,
3535
data: DatabaseData,
3636
originatingClientId: Int? = null
37-
): Int
37+
): Boolean
3838

3939
/** Fetches the data at [storageKey] from the database. */
4040
suspend fun get(

java/arcs/core/storage/driver/Database.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ class DatabaseDriver<Data : Any>(
346346
}
347347

348348
// Store the prepped data.
349-
return (database.insertOrUpdate(storageKey, databaseData, clientId) == version).also {
349+
return database.insertOrUpdate(storageKey, databaseData, clientId).also {
350350
// If the update was successful, update our local data/version.
351351
if (it) localDataMutex.withLock {
352352
localData = data

java/arcs/jvm/storage/database/testutil/MockDatabaseManager.kt

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
package arcs.jvm.storage.database.testutil
1313

14-
import arcs.core.crdt.VersionMap
1514
import arcs.core.data.Schema
1615
import arcs.core.storage.StorageKey
1716
import arcs.core.storage.database.Database
@@ -73,10 +72,11 @@ open class MockDatabase : Database {
7372
storageKey: StorageKey,
7473
data: DatabaseData,
7574
originatingClientId: Int?
76-
): Int = stats.insertUpdate.timeSuspending {
75+
): Boolean = stats.insertUpdate.timeSuspending {
7776
val (version, isNew) = dataMutex.withLock {
7877
val oldData = this.data[storageKey]
79-
if (oldData?.databaseVersion != data.databaseVersion) {
78+
// Must be exactly old version + 1, or have no previous version.
79+
if (oldData == null || data.databaseVersion == oldData.databaseVersion + 1) {
8080
this.data[storageKey] = data
8181
data.databaseVersion to true
8282
} else {
@@ -90,7 +90,7 @@ open class MockDatabase : Database {
9090
.launchIn(CoroutineScope(coroutineContext))
9191
}
9292

93-
return@timeSuspending version
93+
isNew
9494
}
9595

9696
@Suppress("UNCHECKED_CAST")
@@ -99,18 +99,7 @@ open class MockDatabase : Database {
9999
dataType: KClass<out DatabaseData>,
100100
schema: Schema
101101
): DatabaseData? = stats.get.timeSuspending {
102-
val dataVal = dataMutex.withLock { data[storageKey] }
103-
104-
if (dataVal != null) return@timeSuspending dataVal
105-
106-
return@timeSuspending when (dataType) {
107-
DatabaseData.Singleton::class ->
108-
DatabaseData.Singleton(null, schema, -1, VersionMap())
109-
DatabaseData.Collection::class ->
110-
DatabaseData.Collection(emptySet(), schema, -1, VersionMap())
111-
DatabaseData.Entity::class -> null
112-
else -> throw IllegalArgumentException("Illegal type.")
113-
}
102+
dataMutex.withLock { data[storageKey] }
114103
}
115104

116105
override suspend fun delete(storageKey: StorageKey, originatingClientId: Int?) =

javatests/arcs/android/storage/ReferenceModeStoreDatabaseImplIntegrationTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ class ReferenceModeStoreDatabaseImplIntegrationTest {
447447
"t1" to t1Ref
448448
)
449449
),
450-
1
450+
3
451451
)
452452
driver.receiver!!(
453453
CrdtSet.DataImpl(
@@ -458,7 +458,7 @@ class ReferenceModeStoreDatabaseImplIntegrationTest {
458458
"t2" to t2Ref
459459
)
460460
),
461-
2
461+
4
462462
)
463463

464464
activeStore.idle()

javatests/arcs/android/storage/database/DatabaseImplTest.kt

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,11 @@ class DatabaseImplTest {
307307
}
308308

309309
@Test
310-
fun createEntityStorageKeyId_versionNumberMustBeLarger() = runBlockingTest {
310+
fun createEntityStorageKeyId_versionNumberMustBeOneLarger() = runBlockingTest {
311311
val key = DummyStorageKey("key")
312312
val entityId = "entity-id"
313313
val typeId = 123L
314-
database.createEntityStorageKeyId(
314+
val originalStorageKeyId = database.createEntityStorageKeyId(
315315
key,
316316
entityId,
317317
CREATION_TIMESTAMP,
@@ -321,9 +321,10 @@ class DatabaseImplTest {
321321
10,
322322
db
323323
)
324+
assertThat(originalStorageKeyId).isNotNull()
324325

325326
// Same version number is rejected.
326-
val exception1 = assertSuspendingThrows(IllegalArgumentException::class) {
327+
assertThat(
327328
database.createEntityStorageKeyId(
328329
key,
329330
entityId,
@@ -334,14 +335,10 @@ class DatabaseImplTest {
334335
10,
335336
db
336337
)
337-
}
338-
assertThat(exception1).hasMessageThat().isEqualTo(
339-
"Given version (10) must be greater than version in database (10) when updating " +
340-
"storage key dummy://key."
341-
)
338+
).isNull()
342339

343340
// Smaller version number is rejected.
344-
val exception2 = assertSuspendingThrows(IllegalArgumentException::class) {
341+
assertThat(
345342
database.createEntityStorageKeyId(
346343
key,
347344
entityId,
@@ -352,14 +349,24 @@ class DatabaseImplTest {
352349
9,
353350
db
354351
)
355-
}
356-
assertThat(exception2).hasMessageThat().isEqualTo(
357-
"Given version (9) must be greater than version in database (10) when updating " +
358-
"storage key dummy://key."
359-
)
352+
).isNull()
360353

361-
// Increasing version number is ok.
362-
database.createEntityStorageKeyId(
354+
// Increasing version number by more than 1 is rejected.
355+
assertThat(
356+
database.createEntityStorageKeyId(
357+
key,
358+
entityId,
359+
CREATION_TIMESTAMP,
360+
EXPIRATION_TIMESTAMP,
361+
typeId,
362+
VERSION_MAP,
363+
12,
364+
db
365+
)
366+
).isNull()
367+
368+
// Increasing version number by 1 is ok.
369+
val newStorageKeyId = database.createEntityStorageKeyId(
363370
key,
364371
entityId,
365372
CREATION_TIMESTAMP,
@@ -369,6 +376,10 @@ class DatabaseImplTest {
369376
11,
370377
db
371378
)
379+
assertThat(newStorageKeyId).isNotNull()
380+
// TODO: If the storage key is the same, there's no need to delete the old one and create a
381+
// new one.
382+
assertThat(newStorageKeyId).isNotEqualTo(originalStorageKeyId)
372383
}
373384

374385
@Test
@@ -956,15 +967,13 @@ class DatabaseImplTest {
956967
Reference("ref", DummyStorageKey("backing"), VersionMap("ref" to 1))
957968
),
958969
schema = newSchema("hash"),
959-
databaseVersion = 1,
970+
databaseVersion = 2,
960971
versionMap = VERSION_MAP
961972
)
962-
val oldVersion = database.insertOrUpdate(key, collection)
973+
assertThat(database.insertOrUpdate(key, collection)).isTrue()
963974

964-
// TODO - DatabaseImpl should return the last version when an insert/update could not be
965-
// applied.
966-
assertThat(database.insertOrUpdate(key, collection.copy(databaseVersion = oldVersion - 1)))
967-
.isEqualTo(oldVersion - 1)
975+
assertThat(database.insertOrUpdate(key, collection.copy(databaseVersion = 1)))
976+
.isFalse()
968977
}
969978

970979
@Test
@@ -1040,20 +1049,18 @@ class DatabaseImplTest {
10401049
val singleton = DatabaseData.Singleton(
10411050
reference = Reference("ref", DummyStorageKey("backing"), VersionMap("ref" to 1)),
10421051
schema = newSchema("hash"),
1043-
databaseVersion = 1,
1052+
databaseVersion = 2,
10441053
versionMap = VERSION_MAP
10451054
)
1046-
val oldVersion = database.insertOrUpdate(key, singleton, originatingClientId = null)
1055+
assertThat(database.insertOrUpdate(key, singleton, originatingClientId = null)).isTrue()
10471056

1048-
// TODO - DatabaseImpl should return the last version when an insert/update could not be
1049-
// applied.
10501057
assertThat(
10511058
database.insertOrUpdate(
10521059
key,
1053-
singleton.copy(databaseVersion = oldVersion - 1),
1060+
singleton.copy(databaseVersion = 1),
10541061
originatingClientId = null
10551062
)
1056-
).isEqualTo(oldVersion - 1)
1063+
).isFalse()
10571064
}
10581065

10591066
@Test

javatests/arcs/core/storage/ReferenceModeStoreDatabaseIntegrationTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ class ReferenceModeStoreDatabaseIntegrationTest {
432432
"t1" to t1Ref
433433
)
434434
),
435-
1
435+
3
436436
)
437437
driver.receiver!!(
438438
CrdtSet.DataImpl(
@@ -443,7 +443,7 @@ class ReferenceModeStoreDatabaseIntegrationTest {
443443
"t2" to t2Ref
444444
)
445445
),
446-
2
446+
4
447447
)
448448

449449
activeStore.idle()

0 commit comments

Comments
 (0)