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