Special-case AndroidSQLiteDriver connection pool implementation
Due to Android's SDK APIs for SQLite already having a connection pool, Room's connection manager and the used pool should do a pass-through to Android's binding as otherwise the thread-confinement in Android will cause deadlocks due to the pool implementation being Coroutine based. Generally driver implementations are expected to be low-level without any pooling, this is true for all other drivers and hopefully for those implemented by third party, but Android's driver is special in this regards and thus require special consideration.
Also moved out the SupportSQLite connection pool implementation into its own file.
Test: QueryTest
Change-Id: I69d1422231a1ca06afcdd9bbd622dcf303c309ae
diff --git a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/QueryTest.kt b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/QueryTest.kt
index c9ca150..9c8eca8 100644
--- a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/QueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/QueryTest.kt
@@ -17,18 +17,30 @@
package androidx.room.integration.multiplatformtestapp.test
import androidx.room.Room
+import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.driver.AndroidSQLiteDriver
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.Dispatchers
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
-class QueryTest : BaseQueryTest() {
+@RunWith(Parameterized::class)
+class QueryTest(private val driver: SQLiteDriver) : BaseQueryTest() {
private val instrumentation = InstrumentationRegistry.getInstrumentation()
override fun getRoomDatabase(): SampleDatabase {
return Room.inMemoryDatabaseBuilder<SampleDatabase>(context = instrumentation.targetContext)
- .setDriver(BundledSQLiteDriver())
+ .setDriver(driver)
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "driver={0}")
+ fun drivers() = arrayOf(BundledSQLiteDriver(), AndroidSQLiteDriver())
+ }
}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
index a622b72..2e10482 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomConnectionManager.android.kt
@@ -16,17 +16,17 @@
package androidx.room
+import androidx.room.coroutines.AndroidSQLiteDriverConnectionPool
import androidx.room.coroutines.ConnectionPool
-import androidx.room.coroutines.RawConnectionAccessor
import androidx.room.coroutines.newConnectionPool
import androidx.room.coroutines.newSingleConnectionPool
import androidx.room.driver.SupportSQLiteConnection
+import androidx.room.driver.SupportSQLiteConnectionPool
import androidx.room.driver.SupportSQLiteDriver
import androidx.sqlite.SQLiteConnection
-import androidx.sqlite.SQLiteStatement
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
-import androidx.sqlite.use
+import androidx.sqlite.driver.AndroidSQLiteDriver
/**
* An Android platform specific [RoomConnectionManager] with backwards compatibility with
@@ -41,7 +41,7 @@
private val connectionPool: ConnectionPool
internal val supportOpenHelper: SupportSQLiteOpenHelper?
- get() = (connectionPool as? SupportConnectionPool)?.supportDriver?.openHelper
+ get() = (connectionPool as? SupportSQLiteConnectionPool)?.supportDriver?.openHelper
private var supportDatabase: SupportSQLiteDatabase? = null
@@ -64,12 +64,19 @@
.callback(SupportOpenHelperCallback(openDelegate.version))
.build()
this.connectionPool =
- SupportConnectionPool(
+ SupportSQLiteConnectionPool(
SupportSQLiteDriver(config.sqliteOpenHelperFactory.create(openHelperConfig))
)
} else {
this.connectionPool =
- if (configuration.name == null) {
+ if (config.sqliteDriver is AndroidSQLiteDriver) {
+ // Special-case the Android driver and use a pass-through pool since the Android
+ // bindings internally already have a thread-confined connection pool.
+ AndroidSQLiteDriverConnectionPool(
+ driver = DriverWrapper(config.sqliteDriver),
+ fileName = configuration.name ?: ":memory:"
+ )
+ } else if (configuration.name == null) {
// An in-memory database must use a single connection pool.
newSingleConnectionPool(
driver = DriverWrapper(config.sqliteDriver),
@@ -100,7 +107,7 @@
val configWithCompatibilityCallback =
config.installOnOpenCallback { db -> supportDatabase = db }
this.connectionPool =
- SupportConnectionPool(
+ SupportSQLiteConnectionPool(
SupportSQLiteDriver(
supportOpenHelperFactory.invoke(configWithCompatibilityCallback)
)
@@ -184,103 +191,6 @@
}
}
- /**
- * An implementation of a connection pool used in compatibility mode. This impl doesn't do any
- * connection management since the SupportSQLite* APIs already internally do.
- */
- private class SupportConnectionPool(val supportDriver: SupportSQLiteDriver) : ConnectionPool {
- private val supportConnection by
- lazy(LazyThreadSafetyMode.PUBLICATION) {
- val fileName = supportDriver.openHelper.databaseName ?: ":memory:"
- SupportPooledConnection(supportDriver.open(fileName))
- }
-
- override suspend fun <R> useConnection(
- isReadOnly: Boolean,
- block: suspend (Transactor) -> R
- ): R {
- return block.invoke(supportConnection)
- }
-
- override fun close() {
- supportDriver.openHelper.close()
- }
- }
-
- private class SupportPooledConnection(val delegate: SupportSQLiteConnection) :
- Transactor, RawConnectionAccessor {
-
- private var currentTransactionType: Transactor.SQLiteTransactionType? = null
-
- override val rawConnection: SQLiteConnection
- get() = delegate
-
- override suspend fun <R> usePrepared(sql: String, block: (SQLiteStatement) -> R): R {
- return delegate.prepare(sql).use { block.invoke(it) }
- }
-
- // TODO(b/318767291): Add coroutine confinement like RoomDatabase.withTransaction
- override suspend fun <R> withTransaction(
- type: Transactor.SQLiteTransactionType,
- block: suspend TransactionScope<R>.() -> R
- ): R {
- return transaction(type, block)
- }
-
- private suspend fun <R> transaction(
- type: Transactor.SQLiteTransactionType,
- block: suspend TransactionScope<R>.() -> R
- ): R {
- val db = delegate.db
- if (!db.inTransaction()) {
- currentTransactionType = type
- }
- when (type) {
- Transactor.SQLiteTransactionType.DEFERRED -> db.beginTransactionReadOnly()
- Transactor.SQLiteTransactionType.IMMEDIATE -> db.beginTransactionNonExclusive()
- Transactor.SQLiteTransactionType.EXCLUSIVE -> db.beginTransaction()
- }
- try {
- val result = SupportTransactor<R>().block()
- db.setTransactionSuccessful()
- return result
- } catch (rollback: RollbackException) {
- @Suppress("UNCHECKED_CAST") return rollback.result as R
- } finally {
- db.endTransaction()
- if (!db.inTransaction()) {
- currentTransactionType = null
- }
- }
- }
-
- override suspend fun inTransaction(): Boolean {
- return delegate.db.inTransaction()
- }
-
- private class RollbackException(val result: Any?) : Throwable()
-
- private inner class SupportTransactor<T> : TransactionScope<T>, RawConnectionAccessor {
-
- override val rawConnection: SQLiteConnection
- get() = [email protected]
-
- override suspend fun <R> usePrepared(sql: String, block: (SQLiteStatement) -> R): R {
- return [email protected](sql, block)
- }
-
- override suspend fun <R> withNestedTransaction(
- block: suspend (TransactionScope<R>) -> R
- ): R {
- return transaction(checkNotNull(currentTransactionType), block)
- }
-
- override suspend fun rollback(result: T): Nothing {
- throw RollbackException(result)
- }
- }
- }
-
private fun DatabaseConfiguration.installOnOpenCallback(
onOpen: (SupportSQLiteDatabase) -> Unit
): DatabaseConfiguration {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/coroutines/AndroidSQLiteDriverConnectionPool.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/coroutines/AndroidSQLiteDriverConnectionPool.android.kt
new file mode 100644
index 0000000..a815c7e
--- /dev/null
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/coroutines/AndroidSQLiteDriverConnectionPool.android.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.coroutines
+
+import androidx.room.TransactionScope
+import androidx.room.Transactor
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteStatement
+import androidx.sqlite.driver.AndroidSQLiteConnection
+import androidx.sqlite.driver.AndroidSQLiteDriver
+import androidx.sqlite.use
+
+/**
+ * An implementation of a connection pool used when an [AndroidSQLiteDriver] is provided. This impl
+ * doesn't do any connection management since the Android SQLite APIs already internally do.
+ */
+internal class AndroidSQLiteDriverConnectionPool(
+ private val driver: SQLiteDriver,
+ private val fileName: String
+) : ConnectionPool {
+
+ private val androidConnection by lazy {
+ AndroidSQLiteDriverPooledConnection(driver.open(fileName) as AndroidSQLiteConnection)
+ }
+
+ override suspend fun <R> useConnection(
+ isReadOnly: Boolean,
+ block: suspend (Transactor) -> R
+ ): R {
+ return block.invoke(androidConnection)
+ }
+
+ override fun close() {
+ androidConnection.delegate.close()
+ }
+}
+
+private class AndroidSQLiteDriverPooledConnection(val delegate: AndroidSQLiteConnection) :
+ Transactor, RawConnectionAccessor {
+
+ private var currentTransactionType: Transactor.SQLiteTransactionType? = null
+
+ override val rawConnection: SQLiteConnection
+ get() = delegate
+
+ override suspend fun <R> usePrepared(sql: String, block: (SQLiteStatement) -> R): R {
+ return delegate.prepare(sql).use { block.invoke(it) }
+ }
+
+ // TODO(b/318767291): Add coroutine confinement like RoomDatabase.withTransaction
+ override suspend fun <R> withTransaction(
+ type: Transactor.SQLiteTransactionType,
+ block: suspend TransactionScope<R>.() -> R
+ ): R {
+ return transaction(type, block)
+ }
+
+ private suspend fun <R> transaction(
+ type: Transactor.SQLiteTransactionType,
+ block: suspend TransactionScope<R>.() -> R
+ ): R {
+ val db = delegate.db
+ if (!db.inTransaction()) {
+ currentTransactionType = type
+ }
+ when (type) {
+ // TODO(b/288918056): Use Android V API for DEFERRED once it is available
+ Transactor.SQLiteTransactionType.DEFERRED -> db.beginTransactionNonExclusive()
+ Transactor.SQLiteTransactionType.IMMEDIATE -> db.beginTransactionNonExclusive()
+ Transactor.SQLiteTransactionType.EXCLUSIVE -> db.beginTransaction()
+ }
+ try {
+ val result = AndroidSQLiteDriverTransactor<R>().block()
+ db.setTransactionSuccessful()
+ return result
+ } catch (rollback: ConnectionPool.RollbackException) {
+ @Suppress("UNCHECKED_CAST") return rollback.result as R
+ } finally {
+ db.endTransaction()
+ if (!db.inTransaction()) {
+ currentTransactionType = null
+ }
+ }
+ }
+
+ override suspend fun inTransaction(): Boolean {
+ return delegate.db.inTransaction()
+ }
+
+ private inner class AndroidSQLiteDriverTransactor<T> :
+ TransactionScope<T>, RawConnectionAccessor {
+
+ override val rawConnection: SQLiteConnection
+ get() = [email protected]
+
+ override suspend fun <R> usePrepared(sql: String, block: (SQLiteStatement) -> R): R {
+ return [email protected](sql, block)
+ }
+
+ override suspend fun <R> withNestedTransaction(
+ block: suspend (TransactionScope<R>) -> R
+ ): R {
+ return transaction(checkNotNull(currentTransactionType), block)
+ }
+
+ override suspend fun rollback(result: T): Nothing {
+ throw ConnectionPool.RollbackException(result)
+ }
+ }
+}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnectionPool.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnectionPool.android.kt
new file mode 100644
index 0000000..194d4047
--- /dev/null
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/driver/SupportSQLiteConnectionPool.android.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.driver
+
+import androidx.room.TransactionScope
+import androidx.room.Transactor
+import androidx.room.coroutines.ConnectionPool
+import androidx.room.coroutines.RawConnectionAccessor
+import androidx.sqlite.SQLiteConnection
+import androidx.sqlite.SQLiteStatement
+import androidx.sqlite.use
+
+/**
+ * An implementation of a connection pool used in compatibility mode. This impl doesn't do any
+ * connection management since the SupportSQLite* APIs already internally do.
+ */
+internal class SupportSQLiteConnectionPool(internal val supportDriver: SupportSQLiteDriver) :
+ ConnectionPool {
+ private val supportConnection by
+ lazy(LazyThreadSafetyMode.PUBLICATION) {
+ val fileName = supportDriver.openHelper.databaseName ?: ":memory:"
+ SupportSQLitePooledConnection(supportDriver.open(fileName))
+ }
+
+ override suspend fun <R> useConnection(
+ isReadOnly: Boolean,
+ block: suspend (Transactor) -> R
+ ): R {
+ return block.invoke(supportConnection)
+ }
+
+ override fun close() {
+ supportDriver.openHelper.close()
+ }
+}
+
+private class SupportSQLitePooledConnection(val delegate: SupportSQLiteConnection) :
+ Transactor, RawConnectionAccessor {
+
+ private var currentTransactionType: Transactor.SQLiteTransactionType? = null
+
+ override val rawConnection: SQLiteConnection
+ get() = delegate
+
+ override suspend fun <R> usePrepared(sql: String, block: (SQLiteStatement) -> R): R {
+ return delegate.prepare(sql).use { block.invoke(it) }
+ }
+
+ override suspend fun <R> withTransaction(
+ type: Transactor.SQLiteTransactionType,
+ block: suspend TransactionScope<R>.() -> R
+ ): R {
+ return transaction(type, block)
+ }
+
+ private suspend fun <R> transaction(
+ type: Transactor.SQLiteTransactionType,
+ block: suspend TransactionScope<R>.() -> R
+ ): R {
+ val db = delegate.db
+ if (!db.inTransaction()) {
+ currentTransactionType = type
+ }
+ when (type) {
+ Transactor.SQLiteTransactionType.DEFERRED -> db.beginTransactionReadOnly()
+ Transactor.SQLiteTransactionType.IMMEDIATE -> db.beginTransactionNonExclusive()
+ Transactor.SQLiteTransactionType.EXCLUSIVE -> db.beginTransaction()
+ }
+ try {
+ val result = SupportSQLiteTransactor<R>().block()
+ db.setTransactionSuccessful()
+ return result
+ } catch (rollback: ConnectionPool.RollbackException) {
+ @Suppress("UNCHECKED_CAST") return rollback.result as R
+ } finally {
+ db.endTransaction()
+ if (!db.inTransaction()) {
+ currentTransactionType = null
+ }
+ }
+ }
+
+ override suspend fun inTransaction(): Boolean {
+ return delegate.db.inTransaction()
+ }
+
+ private inner class SupportSQLiteTransactor<T> : TransactionScope<T>, RawConnectionAccessor {
+
+ override val rawConnection: SQLiteConnection
+ get() = [email protected]
+
+ override suspend fun <R> usePrepared(sql: String, block: (SQLiteStatement) -> R): R {
+ return [email protected](sql, block)
+ }
+
+ override suspend fun <R> withNestedTransaction(
+ block: suspend (TransactionScope<R>) -> R
+ ): R {
+ return transaction(checkNotNull(currentTransactionType), block)
+ }
+
+ override suspend fun rollback(result: T): Nothing {
+ throw ConnectionPool.RollbackException(result)
+ }
+ }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt
index 7725d48..b69b880 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt
@@ -63,6 +63,9 @@
* the pool is closed.
*/
fun close()
+
+ /** Internal exception thrown to rollback a transaction. */
+ class RollbackException(val result: Any?) : Throwable()
}
/**
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
index 27f43ab..62d9754 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPoolImpl.kt
@@ -320,7 +320,7 @@
return TransactionImpl<R>().block()
} catch (ex: Throwable) {
success = false
- if (ex is RollbackException) {
+ if (ex is ConnectionPool.RollbackException) {
// Type arguments in exception subclasses is not allowed but the exception is always
// created with the correct type.
@Suppress("UNCHECKED_CAST") return (ex.result as R)
@@ -377,8 +377,6 @@
private class TransactionItem(val id: Int, var shouldRollback: Boolean)
- private class RollbackException(val result: Any?) : Throwable()
-
private inner class TransactionImpl<T> : TransactionScope<T>, RawConnectionAccessor {
override val rawConnection: SQLiteConnection
@@ -396,7 +394,7 @@
error("Not in a transaction")
}
delegate.withLock { transactionStack.last().shouldRollback = true }
- throw RollbackException(result)
+ throw ConnectionPool.RollbackException(result)
}
}
diff --git a/sqlite/sqlite-framework/api/restricted_current.txt b/sqlite/sqlite-framework/api/restricted_current.txt
index cc962ad..2a96c29 100644
--- a/sqlite/sqlite-framework/api/restricted_current.txt
+++ b/sqlite/sqlite-framework/api/restricted_current.txt
@@ -10,6 +10,14 @@
package androidx.sqlite.driver {
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class AndroidSQLiteConnection implements androidx.sqlite.SQLiteConnection {
+ ctor public AndroidSQLiteConnection(android.database.sqlite.SQLiteDatabase db);
+ method public void close();
+ method public android.database.sqlite.SQLiteDatabase getDb();
+ method public androidx.sqlite.SQLiteStatement prepare(String sql);
+ property public final android.database.sqlite.SQLiteDatabase db;
+ }
+
public final class AndroidSQLiteDriver implements androidx.sqlite.SQLiteDriver {
ctor public AndroidSQLiteDriver();
method public androidx.sqlite.SQLiteConnection open(String fileName);
diff --git a/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteConnection.android.kt b/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteConnection.android.kt
index 8e8cba8..375a279 100644
--- a/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteConnection.android.kt
+++ b/sqlite/sqlite-framework/src/androidMain/kotlin/androidx/sqlite/driver/AndroidSQLiteConnection.android.kt
@@ -17,12 +17,14 @@
package androidx.sqlite.driver
import android.database.sqlite.SQLiteDatabase
+import androidx.annotation.RestrictTo
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteStatement
import androidx.sqlite.driver.ResultCode.SQLITE_MISUSE
import androidx.sqlite.throwSQLiteException
-internal class AndroidSQLiteConnection(private val db: SQLiteDatabase) : SQLiteConnection {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+class AndroidSQLiteConnection(val db: SQLiteDatabase) : SQLiteConnection {
override fun prepare(sql: String): SQLiteStatement {
if (db.isOpen) {
return AndroidSQLiteStatement.create(db, sql)