Merge "Add a in-process and multi-process lock during database configuration." into androidx-main
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseBuilderTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseBuilderTest.kt
index 08a70e9..dea6886 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseBuilderTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseBuilderTest.kt
@@ -23,6 +23,14 @@
import androidx.sqlite.SQLiteConnection
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.test.Test
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.IO
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
abstract class BaseBuilderTest {
@@ -71,6 +79,68 @@
}
@Test
+ fun onOpenExactlyOnce() = runTest {
+ var onOpenInvoked = 0
+
+ val onOpenBlocker = CompletableDeferred<Unit>()
+ val database = getRoomDatabaseBuilder()
+ .addCallback(
+ object : RoomDatabase.Callback() {
+ // This onOpen callback will block database initialization until the
+ // onOpenLatch is released.
+ override fun onOpen(connection: SQLiteConnection) {
+ onOpenInvoked++
+ runBlocking { onOpenBlocker.await() }
+ }
+ }
+ )
+ .build()
+
+ // Start 4 concurrent coroutines that try to open the database and use its connections,
+ // initialization should be done exactly once
+ val launchBlockers = List(4) { CompletableDeferred<Unit>() }
+ val jobs = List(4) { index ->
+ launch(Dispatchers.IO) {
+ launchBlockers[index].complete(Unit)
+ database.useReaderConnection { }
+ }
+ }
+
+ // Wait all launch coroutines to start then release the latch
+ launchBlockers.awaitAll()
+ delay(100) // A bit more waiting so useReaderConnection reaches the exclusive lock
+ onOpenBlocker.complete(Unit)
+
+ jobs.joinAll()
+ database.close()
+
+ // Initialization should be done exactly once
+ assertThat(onOpenInvoked).isEqualTo(1)
+ }
+
+ @Test
+ fun onOpenRecursive() = runTest {
+ var database: SampleDatabase? = null
+ database = getRoomDatabaseBuilder()
+ .setQueryCoroutineContext(Dispatchers.Unconfined)
+ .addCallback(
+ object : RoomDatabase.Callback() {
+ // Use a bad open callback that will recursively try to open the database
+ // again, this is a user error.
+ override fun onOpen(connection: SQLiteConnection) {
+ runBlocking {
+ checkNotNull(database).dao().getItemList()
+ }
+ }
+ }
+ ).build()
+ assertThrows<IllegalStateException> {
+ database.dao().getItemList()
+ }.hasMessageThat().contains("Recursive database initialization detected.")
+ database.close()
+ }
+
+ @Test
fun setCoroutineContextWithoutDispatcher() {
assertThrows<IllegalArgumentException> {
getRoomDatabaseBuilder().setQueryCoroutineContext(EmptyCoroutineContext)
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index 9d0c517..9b0a135 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -187,6 +187,7 @@
dependsOn(commonTest)
dependencies {
implementation(project(":sqlite:sqlite-bundled"))
+ implementation(libs.okio)
}
}
targets.all { target ->
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/Room.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/Room.android.kt
index d6a942a..e27a6ec 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/Room.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/Room.android.kt
@@ -90,6 +90,11 @@
" If you are trying to create an in memory database, use Room" +
".inMemoryDatabaseBuilder"
}
+ require(name != ":memory:") {
+ "Cannot build a database with the special name ':memory:'." +
+ " If you are trying to create an in memory database, use Room" +
+ ".inMemoryDatabaseBuilder"
+ }
return RoomDatabase.Builder(context, klass, name)
}
@@ -113,7 +118,12 @@
require(name.isNotBlank()) {
"Cannot build a database with empty name." +
" If you are trying to create an in memory database, use Room" +
- ".inMemoryDatabaseBuilder"
+ ".inMemoryDatabaseBuilder()."
+ }
+ require(name != ":memory:") {
+ "Cannot build a database with the special name ':memory:'." +
+ " If you are trying to create an in memory database, use Room" +
+ ".inMemoryDatabaseBuilder()."
}
return RoomDatabase.Builder(T::class, name, factory, context)
}
diff --git a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/BuilderTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/BuilderTest.kt
index 7927ee5..d24b481 100644
--- a/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/BuilderTest.kt
+++ b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/BuilderTest.kt
@@ -80,6 +80,20 @@
}
@Test
+ fun specialMemoryName() {
+ try {
+ databaseBuilder(
+ mock(), RoomDatabase::class.java, ":memory:"
+ ).build()
+ } catch (e: IllegalArgumentException) {
+ assertThat(e.message).isEqualTo(
+ "Cannot build a database with the special name ':memory:'. If you are trying " +
+ "to create an in memory database, use Room.inMemoryDatabaseBuilder"
+ )
+ }
+ }
+
+ @Test
fun executors_setQueryExecutor() {
val executor: Executor = mock()
val db = databaseBuilder(
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
index 0e9e36f..d1f2a0d 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomConnectionManager.kt
@@ -19,6 +19,7 @@
import androidx.annotation.RestrictTo
import androidx.room.RoomDatabase.JournalMode.TRUNCATE
import androidx.room.RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING
+import androidx.room.concurrent.ExclusiveLock
import androidx.room.util.findMigrationPath
import androidx.room.util.isMigrationRequired
import androidx.sqlite.SQLiteConnection
@@ -42,6 +43,12 @@
protected abstract val openDelegate: RoomOpenDelegate
protected abstract val callbacks: List<RoomDatabase.Callback>
+ // Flag indicating that the database was configured, i.e. at least one connection has been
+ // opened, configured and schema validated.
+ private var isConfigured = false
+ // Flag set during initialization to prevent recursive initialization.
+ private var isInitializing = false
+
abstract suspend fun <R> useConnection(
isReadOnly: Boolean,
block: suspend (Transactor) -> R
@@ -51,18 +58,34 @@
protected inner class DriverWrapper(
private val actual: SQLiteDriver
) : SQLiteDriver {
- override fun open(fileName: String): SQLiteConnection {
- return configureConnection(actual.open(fileName))
- }
+ override fun open(fileName: String): SQLiteConnection =
+ ExclusiveLock(
+ filename = fileName,
+ useFileLock = !isConfigured && !isInitializing && fileName != ":memory:"
+ ).withLock {
+ check(!isInitializing) {
+ "Recursive database initialization detected. Did you try to use the database " +
+ "instance during initialization? Maybe in one of the callbacks?"
+ }
+ val connection = actual.open(fileName)
+ if (!isConfigured) {
+ try {
+ isInitializing = true
+ configureConnection(connection)
+ } finally {
+ isInitializing = false
+ }
+ }
+ return@withLock connection
+ }
}
/**
* Common database connection configuration and opening procedure, performs migrations if
* necessary, validates schema and invokes configured callbacks if any.
*/
- // TODO(b/316945717): Thread safe and process safe opening and migration
// TODO(b/316944352): Retry mechanism
- private fun configureConnection(connection: SQLiteConnection): SQLiteConnection {
+ private fun configureConnection(connection: SQLiteConnection) {
configureJournalMode(connection)
val version = connection.prepare("PRAGMA user_version").use { statement ->
statement.step()
@@ -85,7 +108,6 @@
}
}
onOpen(connection)
- return connection
}
private fun configureJournalMode(connection: SQLiteConnection) {
@@ -189,6 +211,7 @@
checkIdentity(connection)
openDelegate.onOpen(connection)
invokeOpenCallback(connection)
+ isConfigured = true
}
private fun checkIdentity(connection: SQLiteConnection) {
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt
new file mode 100644
index 0000000..abd360b
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/ExclusiveLock.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.concurrent
+
+import kotlinx.atomicfu.locks.ReentrantLock
+import kotlinx.atomicfu.locks.SynchronizedObject
+import kotlinx.atomicfu.locks.reentrantLock
+import kotlinx.atomicfu.locks.synchronized
+
+/**
+ * An exclusive lock for in-process and multi-process synchronization.
+ *
+ * The lock is cooperative and only protects the critical region from other [ExclusiveLock] users
+ * with the same `filename`. The lock is reentrant from within the same thread in the same process.
+ *
+ * Locking is done via two levels:
+ * 1. Thread locking within the same process is done via a [ReentrantLock] keyed by the given
+ * `filename`.
+ * 2. Multi-process locking is done via a [FileLock] whose lock file is based on the given
+ * `filename`.
+ *
+ * @param filename The path to the file to protect.
+ * @param useFileLock Whether multi-process lock will be done or not.
+ */
+internal class ExclusiveLock(filename: String, useFileLock: Boolean) {
+ private val threadLock: ReentrantLock = getThreadLock(filename)
+ private val fileLock: FileLock? = if (useFileLock) getFileLock(filename) else null
+
+ fun <T> withLock(block: () -> T): T {
+ threadLock.lock()
+ try {
+ fileLock?.lock()
+ try {
+ return block()
+ } finally {
+ fileLock?.unlock()
+ }
+ } finally {
+ threadLock.unlock()
+ }
+ }
+
+ companion object : SynchronizedObject() {
+ private val threadLocksMap = mutableMapOf<String, ReentrantLock>()
+ private fun getThreadLock(key: String): ReentrantLock = synchronized(this) {
+ return threadLocksMap.getOrPut(key) { reentrantLock() }
+ }
+ private fun getFileLock(key: String) = FileLock(key)
+ }
+}
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/FileLock.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/FileLock.kt
new file mode 100644
index 0000000..7594dc042
--- /dev/null
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/concurrent/FileLock.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.concurrent
+
+/**
+ * A mutually exclusive advisory file lock implementation.
+ *
+ * The lock is cooperative and only protects the critical region from other [FileLock] users in
+ * other process with the same `filename`. Do not use this class on its own, instead use
+ * [ExclusiveLock] which guarantees in-process locking too.
+ *
+ * @param filename The path to the file to protect. Note that an actual lock is not grab on the file
+ * itself but on a temporary file create with the same path but ending with `.lck`.
+ */
+internal expect class FileLock(filename: String) {
+ fun lock()
+ fun unlock()
+}
diff --git a/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/FileLock.jvmAndroid.kt b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/FileLock.jvmAndroid.kt
new file mode 100644
index 0000000..0073b80
--- /dev/null
+++ b/room/room-runtime/src/jvmAndroidMain/kotlin/androidx/room/concurrent/FileLock.jvmAndroid.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.concurrent
+
+import java.io.File
+import java.io.FileOutputStream
+import java.nio.channels.FileChannel
+
+/**
+ * A mutually exclusive advisory file lock implementation.
+ *
+ * The lock is cooperative and only protects the critical region from other [FileLock] users in
+ * other process with the same `filename`. Do not use this class on its own, instead use
+ * [ExclusiveLock] which guarantees in-process locking too.
+ *
+ * @param filename The path to the file to protect. Note that an actual lock is not grab on the file
+ * itself but on a temporary file create with the same path but ending with `.lck`.
+ */
+internal actual class FileLock actual constructor(filename: String) {
+ private val lockFilename = "$filename.lck"
+ private var lockChannel: FileChannel? = null
+
+ actual fun lock() {
+ if (lockChannel != null) {
+ return
+ }
+ try {
+ val lockFile = File(lockFilename)
+ lockFile.parentFile?.mkdirs()
+ lockChannel = FileOutputStream(lockFile).channel
+ lockChannel?.lock()
+ } catch (ex: Throwable) {
+ lockChannel?.close()
+ lockChannel = null
+ throw IllegalStateException("Unable to lock file: '$lockFilename'.", ex)
+ }
+ }
+
+ actual fun unlock() {
+ val channel = lockChannel ?: return
+ try {
+ channel.close()
+ } finally {
+ lockChannel = null
+ }
+ }
+}
diff --git a/room/room-runtime/src/jvmMain/kotlin/androidx/room/Room.jvm.kt b/room/room-runtime/src/jvmMain/kotlin/androidx/room/Room.jvm.kt
index 4626508..d10a21c 100644
--- a/room/room-runtime/src/jvmMain/kotlin/androidx/room/Room.jvm.kt
+++ b/room/room-runtime/src/jvmMain/kotlin/androidx/room/Room.jvm.kt
@@ -60,6 +60,16 @@
name: String,
noinline factory: () -> T = { findAndInstantiateDatabaseImpl(T::class.java) }
): RoomDatabase.Builder<T> {
+ require(name.isNotBlank()) {
+ "Cannot build a database with empty name." +
+ " If you are trying to create an in memory database, use Room" +
+ ".inMemoryDatabaseBuilder()."
+ }
+ require(name != ":memory:") {
+ "Cannot build a database with the special name ':memory:'." +
+ " If you are trying to create an in memory database, use Room" +
+ ".inMemoryDatabaseBuilder()."
+ }
return RoomDatabase.Builder(T::class, name, factory)
}
}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/Room.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/Room.native.kt
index 319dbea..e794c3a 100644
--- a/room/room-runtime/src/nativeMain/kotlin/androidx/room/Room.native.kt
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/Room.native.kt
@@ -56,6 +56,16 @@
name: String,
noinline factory: () -> T
): RoomDatabase.Builder<T> {
+ require(name.isNotBlank()) {
+ "Cannot build a database with empty name." +
+ " If you are trying to create an in memory database, use Room" +
+ ".inMemoryDatabaseBuilder()."
+ }
+ require(name != ":memory:") {
+ "Cannot build a database with the special name ':memory:'." +
+ " If you are trying to create an in memory database, use Room" +
+ ".inMemoryDatabaseBuilder()."
+ }
return RoomDatabase.Builder(T::class, name, factory)
}
}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/FileLock.native.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/FileLock.native.kt
new file mode 100644
index 0000000..3fb8e79
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/concurrent/FileLock.native.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.concurrent
+
+import androidx.room.util.stringError
+import kotlinx.cinterop.cValue
+import kotlinx.cinterop.memScoped
+import platform.posix.F_SETLK
+import platform.posix.F_SETLKW
+import platform.posix.F_UNLCK
+import platform.posix.F_WRLCK
+import platform.posix.O_CREAT
+import platform.posix.O_RDWR
+import platform.posix.SEEK_SET
+import platform.posix.S_IRGRP
+import platform.posix.S_IROTH
+import platform.posix.S_IRUSR
+import platform.posix.S_IWUSR
+import platform.posix.close
+import platform.posix.fcntl
+import platform.posix.flock
+import platform.posix.open
+
+/**
+ * A mutually exclusive advisory file lock implementation.
+ *
+ * The lock is cooperative and only protects the critical region from other [FileLock] users in
+ * other process with the same `filename`. Do not use this class on its own, instead use
+ * [ExclusiveLock] which guarantees in-process locking too.
+ *
+ * @param filename The path to the file to protect. Note that an actual lock is not grab on the file
+ * itself but on a temporary file create with the same path but ending with `.lck`.
+ */
+@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
+internal actual class FileLock actual constructor(filename: String) {
+ private val lockFilename = "$filename.lck"
+ private var lockFd: Int? = null
+
+ actual fun lock() {
+ if (lockFd != null) {
+ return
+ }
+ // Open flags: open in read-write mode and create if doesn't exist
+ // Open mode: user has read / write permissions, group and others only read (0644)
+ val fd = open(lockFilename, O_RDWR or O_CREAT, S_IWUSR or S_IRUSR or S_IRGRP or S_IROTH)
+ check(fd != -1) { "Unable to open lock file (${stringError()}): '$lockFilename'." }
+ try {
+ val cFlock = cValue<flock> {
+ l_type = F_WRLCK.toShort() // acquire write (exclusive) lock
+ l_whence = SEEK_SET.toShort() // lock from start of file
+ l_start = 0 // lock start offset
+ l_len = 0 // lock all bytes (special meaning)
+ }
+ // Command: 'Set lock waiting' will block until file lock is acquired by process
+ if (memScoped { fcntl(fd, F_SETLKW, cFlock.ptr) } == -1) {
+ error("Unable to lock file (${stringError()}): '$lockFilename'.")
+ }
+ lockFd = fd
+ } catch (ex: Throwable) {
+ close(fd)
+ throw ex
+ }
+ }
+
+ actual fun unlock() {
+ val fd = lockFd ?: return
+ try {
+ val cFlock = cValue<flock> {
+ l_type = F_UNLCK.toShort() // release lock
+ l_whence = SEEK_SET.toShort() // lock from start of file
+ l_start = 0 // lock start offset
+ l_len = 0 // lock all bytes (special meaning)
+ }
+ // Command: 'Set lock' (without waiting because we are unlocking)
+ if (memScoped { fcntl(fd, F_SETLK, cFlock.ptr) } == -1) {
+ error("Unable to unlock file (${stringError()}): '$lockFilename'.")
+ }
+ } finally {
+ close(fd)
+ }
+ lockFd = null
+ }
+}
diff --git a/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/PosixUtil.kt b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/PosixUtil.kt
new file mode 100644
index 0000000..8a4ff37
--- /dev/null
+++ b/room/room-runtime/src/nativeMain/kotlin/androidx/room/util/PosixUtil.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.util
+
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.toKString
+import platform.posix.errno
+import platform.posix.strerror
+
+/**
+ * Convenience function to get a String description of the last error number.
+ */
+@OptIn(ExperimentalForeignApi::class)
+fun stringError(): String = strerror(errno)?.toKString() ?: "Unknown error"
diff --git a/room/room-runtime/src/nativeTest/kotlin/androidx.room/concurrent/FileLockTest.kt b/room/room-runtime/src/nativeTest/kotlin/androidx.room/concurrent/FileLockTest.kt
new file mode 100644
index 0000000..2b25cb9
--- /dev/null
+++ b/room/room-runtime/src/nativeTest/kotlin/androidx.room/concurrent/FileLockTest.kt
@@ -0,0 +1,149 @@
+/*
+ * 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.concurrent
+
+import androidx.kruth.assertThat
+import androidx.kruth.assertWithMessage
+import androidx.room.util.stringError
+import kotlin.random.Random
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlinx.cinterop.ExperimentalForeignApi
+import kotlinx.cinterop.alloc
+import kotlinx.cinterop.memScoped
+import kotlinx.cinterop.ptr
+import okio.FileSystem
+import okio.Path.Companion.toPath
+import platform.posix.exit
+import platform.posix.fork
+import platform.posix.gettimeofday
+import platform.posix.remove
+import platform.posix.timeval
+import platform.posix.usleep
+import platform.posix.waitpid
+
+@OptIn(ExperimentalForeignApi::class)
+class FileLockTest {
+
+ private val testFile = "/tmp/test-${Random.nextInt()}.db"
+ private val parentLogFile = "/tmp/test-${Random.nextInt()}-parent"
+ private val childLogFile = "/tmp/test-${Random.nextInt()}-child"
+
+ @BeforeTest
+ fun before() {
+ remove(testFile)
+ remove(parentLogFile)
+ remove(childLogFile)
+ }
+
+ @AfterTest
+ fun after() {
+ remove(testFile)
+ remove(parentLogFile)
+ remove(childLogFile)
+ }
+
+ @Test
+ fun processLock() {
+ val pid = fork()
+ if (pid == -1) {
+ error("fork() failed: ${stringError()}")
+ }
+
+ // Forked code is next, both process concurrently attempt to acquire the file lock,
+ // recording the time they did so to later validate exclusive access to the critical
+ // region.
+ val timeStamps = TimeStamps()
+ val lock = FileLock(testFile)
+ timeStamps.before = getUnixMicroseconds()
+ lock.lock()
+ // sleep for 200ms total to give a chance for contention
+ usleep(100u * 1000u)
+ timeStamps.during = getUnixMicroseconds()
+ usleep(100u * 1000u)
+ lock.unlock()
+ timeStamps.after = getUnixMicroseconds()
+ writeTimestamps(
+ logFile = if (pid == 0) childLogFile else parentLogFile,
+ timeStamps = timeStamps,
+ )
+
+ when (pid) {
+ // Child process, terminate
+ 0 -> {
+ exit(0)
+ }
+ // Parent process (this test), wait for child
+ else -> {
+ val result = waitpid(pid, null, 0)
+ if (result == -1) {
+ error("wait() failed: ${stringError()}")
+ }
+ }
+ }
+
+ val parentTimeStamps = readTimestamps(parentLogFile)
+ val childTimeStamps = readTimestamps(childLogFile)
+
+ // Initial check, attempt was before acquire, and release was after acquired
+ assertThat(parentTimeStamps.before).isLessThan(parentTimeStamps.during)
+ assertThat(parentTimeStamps.during).isLessThan(parentTimeStamps.after)
+ assertThat(childTimeStamps.before).isLessThan(childTimeStamps.during)
+ assertThat(childTimeStamps.during).isLessThan(childTimeStamps.after)
+
+ // Find out who got the lock first
+ val (first, second) = if (parentTimeStamps.during < childTimeStamps.during) {
+ parentTimeStamps to childTimeStamps
+ } else {
+ childTimeStamps to parentTimeStamps
+ }
+ // Now really validate second acquired the lock *after* first released it
+ assertWithMessage("Comparing first unlock time with second acquire time")
+ .that(first.after).isLessThan(second.during)
+ }
+
+ private fun writeTimestamps(logFile: String, timeStamps: TimeStamps) {
+ FileSystem.SYSTEM.write(logFile.toPath(), mustCreate = true) {
+ writeUtf8("${timeStamps.before},${timeStamps.during},${timeStamps.after}")
+ }
+ }
+
+ private fun readTimestamps(logFile: String): TimeStamps {
+ return FileSystem.SYSTEM.read(logFile.toPath()) {
+ val stamps = readUtf8().split(",")
+ TimeStamps(
+ before = stamps[0].toLong(),
+ during = stamps[1].toLong(),
+ after = stamps[2].toLong()
+ )
+ }
+ }
+
+ // All times are in microseconds
+ data class TimeStamps(
+ var before: Long = -1,
+ var during: Long = -1,
+ var after: Long = -1
+ )
+
+ private fun getUnixMicroseconds(): Long = memScoped {
+ val tv = alloc<timeval>()
+ gettimeofday(tv.ptr, null)
+ return (tv.tv_sec * 1000000) + tv.tv_usec
+ }
+}
diff --git a/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt b/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt
index 9e4a324..f62ae11 100644
--- a/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt
+++ b/room/room-testing/src/androidMain/kotlin/androidx/room/testing/MigrationTestHelper.android.kt
@@ -543,7 +543,15 @@
this.driverWrapper = DriverWrapper(supportDriver)
}
- override fun openConnection() = driverWrapper.open(configuration.name ?: ":memory:")
+ override fun openConnection(): SQLiteConnection {
+ val name = configuration.name
+ val filename = if (configuration.name != null) {
+ configuration.context.getDatabasePath(name).absolutePath
+ } else {
+ ":memory:"
+ }
+ return driverWrapper.open(filename)
+ }
inner class SupportOpenHelperCallback(
version: Int