Handling multiple open events for the same db
Handling multiple open events for the same database.
Deduplication logic:
- For file based databases verifying path
- For in-memory databases treating each new instance as a different db.
Keeping databases open indefinitely until we start tracking.
database-closed-events.
Bug: 143216096
Bug: 153695085
Test: ./gradlew :sqlite:sqlite-inspection:cC
Change-Id: Ie00bf98a5c12170f1d782d7915ad305ebe917108
diff --git a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/DatabaseExtensions.kt b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/DatabaseExtensions.kt
index 18bb086..3b3d06b 100644
--- a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/DatabaseExtensions.kt
+++ b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/DatabaseExtensions.kt
@@ -21,6 +21,7 @@
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.rules.TemporaryFolder
+import java.io.File
fun SQLiteDatabase.addTable(table: Table) = execSQL(table.toCreateString())
@@ -34,7 +35,11 @@
}
fun Database.createInstance(temporaryFolder: TemporaryFolder): SQLiteDatabase {
- val path = temporaryFolder.newFile(this.name).absolutePath
+ val path = if (name == null) null else
+ File(temporaryFolder.root, name)
+ .also { it.createNewFile() } // can handle an existing file
+ .absolutePath
+
val context = ApplicationProvider.getApplicationContext() as android.content.Context
val openHelper = object : SQLiteOpenHelper(context, path, null, 1) {
override fun onCreate(db: SQLiteDatabase?) = Unit
diff --git a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/Model.kt b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/Model.kt
index b1b324a..fb46d79 100644
--- a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/Model.kt
+++ b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/Model.kt
@@ -16,8 +16,8 @@
package androidx.sqlite.inspection.test
-data class Database(val name: String, val tables: List<Table>) {
- constructor(name: String, vararg tables: Table) : this(name, tables.toList())
+data class Database(val name: String?, val tables: List<Table>) {
+ constructor(name: String?, vararg tables: Table) : this(name, tables.toList())
}
data class Table(val name: String, val columns: List<Column>) {
diff --git a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/TrackDatabasesTest.kt b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/TrackDatabasesTest.kt
index c3f783e..47da14a 100644
--- a/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/TrackDatabasesTest.kt
+++ b/sqlite/sqlite-inspection/src/androidTest/java/androidx/sqlite/inspection/test/TrackDatabasesTest.kt
@@ -18,6 +18,7 @@
import android.database.sqlite.SQLiteDatabase
import androidx.inspection.InspectorEnvironment.ExitHook
+import androidx.sqlite.inspection.SqliteInspectorProtocol.Event
import androidx.sqlite.inspection.test.MessageFactory.createTrackDatabasesCommand
import androidx.sqlite.inspection.test.MessageFactory.createTrackDatabasesResponse
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -71,7 +72,8 @@
// evaluate registered hooks
val hookEntries = testEnvironment.consumeRegisteredHooks()
- assertThat(hookEntries).hasSize(10)
+ .filter { it.originMethod == OPEN_DATABASE_COMMAND_SIGNATURE }
+ assertThat(hookEntries).hasSize(1)
hookEntries.first().let { entry ->
// expect one exit hook tracking database open events
assertThat(entry).isInstanceOf(Hook.ExitHook::class.java)
@@ -94,31 +96,96 @@
}
@Test
- fun test_track_databases_the_same_database_opened_twice() = runBlocking {
+ fun test_track_databases_the_same_database_opened_multiple_times() = runBlocking {
+ // given
testEnvironment.sendCommand(createTrackDatabasesCommand())
- val hooks = testEnvironment.consumeRegisteredHooks()
- assertThat(hooks).hasSize(10)
-
- val onOpenHook = hooks.first()
- assertThat(onOpenHook.originMethod).isEqualTo(OPEN_DATABASE_COMMAND_SIGNATURE)
- val database = Database("db").createInstance(temporaryFolder)
+ val onOpenHook = testEnvironment.consumeRegisteredHooks()
+ .first { it.originMethod == OPEN_DATABASE_COMMAND_SIGNATURE }
@Suppress("UNCHECKED_CAST")
- val onExit = (onOpenHook.asExitHook as ExitHook<SQLiteDatabase>)::onExit
+ val onOpen = (onOpenHook.asExitHook as ExitHook<SQLiteDatabase>)::onExit
- // open event on a database first time
- onExit(database)
- testEnvironment.receiveEvent()
- .let { event -> assertThat(event.hasDatabaseOpened()).isEqualTo(true) }
+ val seenDbIds = mutableSetOf<Int>()
- // open event on the same database for the second time
- // TODO: track close database events or handle the below gracefully
- onExit(database)
- testEnvironment.receiveEvent().let { event ->
- assertThat(event.hasErrorOccurred()).isEqualTo(true)
- val error = event.errorOccurred.content
- assertThat(error.message).contains("Database is already tracked")
- assertThat(error.message).contains(database.path)
- assertThat(error.recoverability.isRecoverable).isEqualTo(false)
+ fun checkDbOpenedEvent(event: Event, database: SQLiteDatabase) {
+ assertThat(event.hasDatabaseOpened()).isEqualTo(true)
+ val isNewId = seenDbIds.add(event.databaseOpened.databaseId)
+ assertThat(isNewId).isEqualTo(true)
+ assertThat(event.databaseOpened.name).isEqualTo(database.path)
}
+
+ // file based db: first open
+ val fileDbPath = "db1"
+ val fileDb = Database(fileDbPath).createInstance(temporaryFolder)
+ onOpen(fileDb)
+ checkDbOpenedEvent(testEnvironment.receiveEvent(), fileDb)
+
+ // file based db: same instance
+ onOpen(fileDb)
+ testEnvironment.assertNoQueuedEvents()
+
+ // file based db: same path
+ onOpen(Database(fileDbPath).createInstance(temporaryFolder))
+ testEnvironment.assertNoQueuedEvents()
+
+ // in-memory database: first open
+ val inMemDb = Database(null).createInstance(temporaryFolder)
+ onOpen(inMemDb)
+ checkDbOpenedEvent(testEnvironment.receiveEvent(), inMemDb)
+
+ // in-memory database: same instance
+ onOpen(inMemDb)
+ testEnvironment.assertNoQueuedEvents()
+
+ // in-memory database: new instances (same path = :memory:)
+ repeat(3) {
+ val db = Database(null).createInstance(temporaryFolder)
+ assertThat(db.path).isEqualTo(":memory:")
+ onOpen(db)
+ checkDbOpenedEvent(testEnvironment.receiveEvent(), db)
+ }
+ }
+
+ @Test
+ fun test_track_databases_keeps_db_open() = runBlocking {
+ // given
+ testEnvironment.sendCommand(createTrackDatabasesCommand())
+ val onOpenHook = testEnvironment.consumeRegisteredHooks()
+ .first { it.originMethod == OPEN_DATABASE_COMMAND_SIGNATURE }
+
+ @Suppress("UNCHECKED_CAST")
+ val onOpen = (onOpenHook.asExitHook as ExitHook<SQLiteDatabase>)::onExit
+
+ // without inspecting
+ val dbNoInspecting = Database("dbNoInspecting").createInstance(temporaryFolder)
+ dbNoInspecting.close()
+ assertThat(dbNoInspecting.isOpen).isFalse()
+
+ // with inspecting
+ listOf("db2", null).forEach { name ->
+ val dbWithInspecting = Database(name).createInstance(temporaryFolder)
+ onOpen(dbWithInspecting) // start inspecting
+ dbWithInspecting.close()
+ assertThat(dbWithInspecting.isOpen).isTrue()
+ }
+ }
+
+ @Test
+ fun test_temporary_databases_same_path_different_database() {
+ // given
+ val db1 = Database(null).createInstance(temporaryFolder)
+ val db2 = Database(null).createInstance(temporaryFolder)
+ fun queryTableCount(db: SQLiteDatabase): Long =
+ db.compileStatement("select count(*) from sqlite_master").simpleQueryForLong()
+ assertThat(queryTableCount(db1)).isEqualTo(1) // android_metadata sole table
+ assertThat(queryTableCount(db2)).isEqualTo(1) // android_metadata sole table
+ assertThat(db1.path).isEqualTo(db2.path)
+ assertThat(db1.path).isEqualTo(":memory:")
+
+ // when
+ db1.execSQL("create table t1 (c1 int)")
+
+ // then
+ assertThat(queryTableCount(db1)).isEqualTo(2)
+ assertThat(queryTableCount(db2)).isEqualTo(1)
}
}
diff --git a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java
index 0034550..abf1e1b 100644
--- a/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java
+++ b/sqlite/sqlite-inspection/src/main/java/androidx/sqlite/inspection/SqliteInspector.java
@@ -66,6 +66,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
@@ -515,21 +516,15 @@
// avoiding a synthetic accessor
void onDatabaseAdded(SQLiteDatabase database) {
Event response;
- try {
- int id = mDatabaseRegistry.addDatabase(database);
- String name = database.getPath();
- response = createDatabaseOpenedEvent(id, name);
- mRoomInvalidationRegistry.invalidateCache();
- } catch (IllegalArgumentException exception) {
- String message = exception.getMessage();
- // TODO: clean up, e.g. replace Exception message check with a custom Exception class
- if (message != null && message.contains("Database is already tracked")) {
- response = createErrorOccurredEvent(exception, null);
- } else {
- throw exception;
- }
- }
+ int id = mDatabaseRegistry.addDatabase(database);
+ if (id == DatabaseRegistry.ALREADY_TRACKED) return; // Nothing to do
+ // TODO: replace with db open/closed tracking as this will keep the database open
+ database.acquireReference();
+
+ String name = database.getPath();
+ response = createDatabaseOpenedEvent(id, name);
+ mRoomInvalidationRegistry.invalidateCache();
getConnection().sendEvent(response.toByteArray());
}
@@ -551,11 +546,6 @@
.build();
}
- private Event createErrorOccurredEvent(@NonNull Exception exception, Boolean isRecoverable) {
- return createErrorOccurredEvent(exception.getMessage(), stackTraceFromException(exception),
- isRecoverable);
- }
-
private static ErrorContent createErrorContentMessage(@Nullable String message,
@Nullable String stackTrace, Boolean isRecoverable) {
ErrorContent.Builder builder = ErrorContent.newBuilder();
@@ -597,6 +587,9 @@
}
static class DatabaseRegistry {
+ private static final String IN_MEMORY_DB_PATH = ":memory:";
+ static final int ALREADY_TRACKED = -1;
+
private final Object mLock = new Object();
// starting from '1' to distinguish from '0' which could stand for an unset parameter
@@ -608,21 +601,24 @@
* Thread safe
*
* @return id used to track the database
- * @throws IllegalArgumentException if database is already in the registry
*/
int addDatabase(@NonNull SQLiteDatabase database) {
synchronized (mLock) {
- // TODO: decide if compare by path or object-reference; for now using reference
- // TODO: decide if the same database object here twice an Exception
// TODO: decide if to track database close events and update here
// TODO: decide if use weak-references to database objects
// TODO: consider database.acquireReference() approach
// check if already tracked
for (Map.Entry<Integer, SQLiteDatabase> entry : mDatabases.entrySet()) {
+ // Instance already tracked
if (entry.getValue() == database) {
- throw new IllegalArgumentException(
- "Database is already tracked: " + database.getPath());
+ return ALREADY_TRACKED;
+ }
+ // Path already tracked (and not an in-memory database)
+ final String path = database.getPath();
+ if (!Objects.equals(IN_MEMORY_DB_PATH, path)
+ && Objects.equals(path, entry.getValue().getPath())) {
+ return ALREADY_TRACKED;
}
}