From 015f810a420832954d91dfe47b4380ab23cabd73 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Fri, 24 Apr 2026 11:47:23 -0700 Subject: [PATCH 1/6] Fix race condition and add DB guard for LSP indexing --- .../codeonthego/indexing/SQLiteIndex.kt | 13 +++++++++++++ .../codeonthego/indexing/util/BackgroundIndexer.kt | 3 ++- .../codeonthego/indexing/jvm/JvmSymbolIndex.kt | 6 +----- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt index 786dca5c03..df203a5976 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt @@ -9,6 +9,7 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.appdevforall.codeonthego.indexing.api.Index +import java.util.concurrent.atomic.AtomicBoolean import org.appdevforall.codeonthego.indexing.api.IndexDescriptor import org.appdevforall.codeonthego.indexing.api.IndexQuery import org.appdevforall.codeonthego.indexing.api.Indexable @@ -71,6 +72,7 @@ class SQLiteIndex( .filter { it.prefixSearchable } .associate { it.name to "f_${it.name}_lower" } + private val closed = AtomicBoolean(false) private val db: SupportSQLiteDatabase init { @@ -103,6 +105,7 @@ class SQLiteIndex( } override fun query(query: IndexQuery): Sequence { + if (closed.get()) return emptySequence() val (sql, args) = buildSelectQuery(query) val cursor = db.query(sql, args.toTypedArray()) return cursor.use { @@ -116,6 +119,7 @@ class SQLiteIndex( } override suspend fun get(key: String): T? = withContext(Dispatchers.IO) { + if (closed.get()) return@withContext null val cursor = db.query( "SELECT _payload FROM $tableName WHERE _key = ? LIMIT 1", arrayOf(key), @@ -129,6 +133,7 @@ class SQLiteIndex( override suspend fun containsSource(sourceId: String): Boolean = withContext(Dispatchers.IO) { + if (closed.get()) return@withContext false val cursor = db.query( "SELECT 1 FROM $tableName WHERE _source_id = ? LIMIT 1", arrayOf(sourceId), @@ -137,6 +142,7 @@ class SQLiteIndex( } override fun distinctValues(fieldName: String): Sequence { + if (closed.get()) return emptySequence() val col = fieldColumns[fieldName] ?: throw IllegalArgumentException("Unknown field: $fieldName") val cursor = db.query("SELECT DISTINCT $col FROM $tableName WHERE $col IS NOT NULL") @@ -150,6 +156,7 @@ class SQLiteIndex( } override suspend fun insertAll(entries: Sequence) = withContext(Dispatchers.IO) { + if (closed.get()) return@withContext val batch = mutableListOf() for (entry in entries) { batch.add(entry) @@ -164,22 +171,27 @@ class SQLiteIndex( } override suspend fun insert(entry: T) = withContext(Dispatchers.IO) { + if (closed.get()) return@withContext insertBatch(listOf(entry)) } override suspend fun removeBySource(sourceId: String) = withContext(Dispatchers.IO) { + if (closed.get()) return@withContext db.execSQL("DELETE FROM $tableName WHERE _source_id = ?", arrayOf(sourceId)) } override suspend fun clear() = withContext(Dispatchers.IO) { + if (closed.get()) return@withContext db.execSQL("DELETE FROM $tableName") } override fun close() { + if (closed.getAndSet(true)) return db.close() } suspend fun size(): Int = withContext(Dispatchers.IO) { + if (closed.get()) return@withContext 0 val cursor = db.query("SELECT COUNT(*) FROM $tableName") cursor.use { if (it.moveToFirst()) it.getInt(0) else 0 } } @@ -225,6 +237,7 @@ class SQLiteIndex( } private fun insertBatch(entries: List) { + if (closed.get()) return db.beginTransaction() try { for (entry in entries) { diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt index 3d2e01a6ae..5b6d321aba 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.isActive import kotlinx.coroutines.joinAll @@ -163,7 +164,7 @@ class BackgroundIndexer( val activeJobCount: Int get() = activeJobs.size override fun close() { - activeJobs.values.forEach { it.cancel() } + scope.cancel("BackgroundIndexer closed") activeJobs.clear() } } diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt index 75b9019ba6..53dd8463ac 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JvmSymbolIndex.kt @@ -139,11 +139,7 @@ class JvmSymbolIndex( suspend fun awaitIndexing() = indexer.awaitAll() override fun close() { - super.close() - if (backing is AutoCloseable) { - backing.close() - } - indexer.close() + super.close() } } From d7fb991ec98636f7abafd2c3d224bb43f0764bba Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Fri, 24 Apr 2026 12:24:35 -0700 Subject: [PATCH 2/6] Added lock-based thread safety to SQLiteIndex and synchronous shutdown to BackgroundIndexer to fully close the race condition window between in-flight DB --- .../codeonthego/indexing/SQLiteIndex.kt | 85 ++++++++++--------- .../indexing/util/BackgroundIndexer.kt | 11 +-- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt index df203a5976..178a98013e 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt @@ -9,7 +9,9 @@ import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.appdevforall.codeonthego.indexing.api.Index -import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write import org.appdevforall.codeonthego.indexing.api.IndexDescriptor import org.appdevforall.codeonthego.indexing.api.IndexQuery import org.appdevforall.codeonthego.indexing.api.Indexable @@ -72,7 +74,8 @@ class SQLiteIndex( .filter { it.prefixSearchable } .associate { it.name to "f_${it.name}_lower" } - private val closed = AtomicBoolean(false) + private val lock = ReentrantReadWriteLock() + @Volatile private var closed = false private val db: SupportSQLiteDatabase init { @@ -104,11 +107,10 @@ class SQLiteIndex( createTable(db) } - override fun query(query: IndexQuery): Sequence { - if (closed.get()) return emptySequence() + override fun query(query: IndexQuery): Sequence = ifOpen(emptySequence()) { val (sql, args) = buildSelectQuery(query) val cursor = db.query(sql, args.toTypedArray()) - return cursor.use { + cursor.use { val payloadIdx = it.getColumnIndexOrThrow("_payload") buildList { while (it.moveToNext()) { @@ -119,34 +121,35 @@ class SQLiteIndex( } override suspend fun get(key: String): T? = withContext(Dispatchers.IO) { - if (closed.get()) return@withContext null - val cursor = db.query( - "SELECT _payload FROM $tableName WHERE _key = ? LIMIT 1", - arrayOf(key), - ) - cursor.use { - if (it.moveToFirst()) { - descriptor.deserialize(it.getBlob(0)) - } else null + ifOpen(null) { + val cursor = db.query( + "SELECT _payload FROM $tableName WHERE _key = ? LIMIT 1", + arrayOf(key), + ) + cursor.use { + if (it.moveToFirst()) { + descriptor.deserialize(it.getBlob(0)) + } else null + } } } override suspend fun containsSource(sourceId: String): Boolean = withContext(Dispatchers.IO) { - if (closed.get()) return@withContext false - val cursor = db.query( - "SELECT 1 FROM $tableName WHERE _source_id = ? LIMIT 1", - arrayOf(sourceId), - ) - cursor.use { it.moveToFirst() } + ifOpen(false) { + val cursor = db.query( + "SELECT 1 FROM $tableName WHERE _source_id = ? LIMIT 1", + arrayOf(sourceId), + ) + cursor.use { it.moveToFirst() } + } } - override fun distinctValues(fieldName: String): Sequence { - if (closed.get()) return emptySequence() + override fun distinctValues(fieldName: String): Sequence = ifOpen(emptySequence()) { val col = fieldColumns[fieldName] ?: throw IllegalArgumentException("Unknown field: $fieldName") val cursor = db.query("SELECT DISTINCT $col FROM $tableName WHERE $col IS NOT NULL") - return cursor.use { + cursor.use { buildList { while (it.moveToNext()) { add(it.getString(0)) @@ -156,44 +159,51 @@ class SQLiteIndex( } override suspend fun insertAll(entries: Sequence) = withContext(Dispatchers.IO) { - if (closed.get()) return@withContext val batch = mutableListOf() for (entry in entries) { batch.add(entry) if (batch.size >= batchSize) { - insertBatch(batch) + ifOpen { insertBatch(batch) } batch.clear() } } if (batch.isNotEmpty()) { - insertBatch(batch) + ifOpen { insertBatch(batch) } } } override suspend fun insert(entry: T) = withContext(Dispatchers.IO) { - if (closed.get()) return@withContext - insertBatch(listOf(entry)) + ifOpen { insertBatch(listOf(entry)) } } override suspend fun removeBySource(sourceId: String) = withContext(Dispatchers.IO) { - if (closed.get()) return@withContext - db.execSQL("DELETE FROM $tableName WHERE _source_id = ?", arrayOf(sourceId)) + ifOpen { db.execSQL("DELETE FROM $tableName WHERE _source_id = ?", arrayOf(sourceId)) } } override suspend fun clear() = withContext(Dispatchers.IO) { - if (closed.get()) return@withContext - db.execSQL("DELETE FROM $tableName") + ifOpen { db.execSQL("DELETE FROM $tableName") } } override fun close() { - if (closed.getAndSet(true)) return - db.close() + lock.write { + if (closed) return + closed = true + db.close() + } + } + + private inline fun ifOpen(default: R, block: () -> R): R = + lock.read { if (closed) default else block() } + + private inline fun ifOpen(block: () -> Unit) { + lock.read { if (!closed) block() } } suspend fun size(): Int = withContext(Dispatchers.IO) { - if (closed.get()) return@withContext 0 - val cursor = db.query("SELECT COUNT(*) FROM $tableName") - cursor.use { if (it.moveToFirst()) it.getInt(0) else 0 } + ifOpen(0) { + val cursor = db.query("SELECT COUNT(*) FROM $tableName") + cursor.use { if (it.moveToFirst()) it.getInt(0) else 0 } + } } private fun createTable(db: SupportSQLiteDatabase) { @@ -237,7 +247,6 @@ class SQLiteIndex( } private fun insertBatch(entries: List) { - if (closed.get()) return db.beginTransaction() try { for (entry in entries) { diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt index 5b6d321aba..90e893a9bf 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt @@ -5,11 +5,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.isActive import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.appdevforall.codeonthego.indexing.api.Index import org.appdevforall.codeonthego.indexing.api.Indexable import org.slf4j.LoggerFactory @@ -44,11 +44,12 @@ sealed class IndexingEvent { */ class BackgroundIndexer( private val index: Index, - private val scope: CoroutineScope = CoroutineScope( - SupervisorJob() + Dispatchers.Default - ), + parentScope: CoroutineScope = CoroutineScope(Dispatchers.Default), ) : Closeable { + private val job = SupervisorJob(parentScope.coroutineContext[Job]) + private val scope = CoroutineScope(parentScope.coroutineContext + job) + companion object { private val log = LoggerFactory.getLogger(BackgroundIndexer::class.java) } @@ -164,7 +165,7 @@ class BackgroundIndexer( val activeJobCount: Int get() = activeJobs.size override fun close() { - scope.cancel("BackgroundIndexer closed") + runBlocking { job.cancelAndJoin() } activeJobs.clear() } } From 3b2f2fc9af6040bb43d3a886b91202574d56ec62 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Fri, 24 Apr 2026 14:03:06 -0700 Subject: [PATCH 3/6] Fix a potential ANR in the main thread --- .../codeonthego/indexing/util/BackgroundIndexer.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt index 90e893a9bf..570e1aa624 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull import org.appdevforall.codeonthego.indexing.api.Index import org.appdevforall.codeonthego.indexing.api.Indexable import org.slf4j.LoggerFactory @@ -52,6 +53,7 @@ class BackgroundIndexer( companion object { private val log = LoggerFactory.getLogger(BackgroundIndexer::class.java) + private const val CLOSE_TIMEOUT_MS = 5_000L } var progressListener: IndexingProgressListener? = null @@ -165,7 +167,15 @@ class BackgroundIndexer( val activeJobCount: Int get() = activeJobs.size override fun close() { - runBlocking { job.cancelAndJoin() } + runBlocking { + val completed = withTimeoutOrNull(CLOSE_TIMEOUT_MS) { + job.cancelAndJoin() + } + if (completed == null) { + log.warn("Indexer close timed out after {}ms, force-cancelling", CLOSE_TIMEOUT_MS) + job.cancel() + } + } activeJobs.clear() } } From ab21bcef96d0622d709cd95d116459bd41b24c5f Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Sat, 25 Apr 2026 12:47:44 -0700 Subject: [PATCH 4/6] Remove ReentrantReadWriteLock() with kotlinx.coroutines.sync.Mutex; make BackgroundIndexer.close wait for cancellation completion and update misleading log text --- .../codeonthego/indexing/SQLiteIndex.kt | 71 ++++++++++--------- .../indexing/util/BackgroundIndexer.kt | 17 +++-- 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt index 178a98013e..cce8760da4 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt @@ -7,11 +7,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.appdevforall.codeonthego.indexing.api.Index -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.read -import kotlin.concurrent.write import org.appdevforall.codeonthego.indexing.api.IndexDescriptor import org.appdevforall.codeonthego.indexing.api.IndexQuery import org.appdevforall.codeonthego.indexing.api.Indexable @@ -74,7 +74,7 @@ class SQLiteIndex( .filter { it.prefixSearchable } .associate { it.name to "f_${it.name}_lower" } - private val lock = ReentrantReadWriteLock() + private val mutex = Mutex() @Volatile private var closed = false private val db: SupportSQLiteDatabase @@ -107,17 +107,19 @@ class SQLiteIndex( createTable(db) } - override fun query(query: IndexQuery): Sequence = ifOpen(emptySequence()) { - val (sql, args) = buildSelectQuery(query) - val cursor = db.query(sql, args.toTypedArray()) - cursor.use { - val payloadIdx = it.getColumnIndexOrThrow("_payload") - buildList { - while (it.moveToNext()) { - add(descriptor.deserialize(it.getBlob(payloadIdx))) + override fun query(query: IndexQuery): Sequence = runBlocking { + ifOpen(emptySequence()) { + val (sql, args) = buildSelectQuery(query) + val cursor = db.query(sql, args.toTypedArray()) + cursor.use { + val payloadIdx = it.getColumnIndexOrThrow("_payload") + buildList { + while (it.moveToNext()) { + add(descriptor.deserialize(it.getBlob(payloadIdx))) + } } - } - }.asSequence() + }.asSequence() + } } override suspend fun get(key: String): T? = withContext(Dispatchers.IO) { @@ -145,17 +147,19 @@ class SQLiteIndex( } } - override fun distinctValues(fieldName: String): Sequence = ifOpen(emptySequence()) { - val col = fieldColumns[fieldName] - ?: throw IllegalArgumentException("Unknown field: $fieldName") - val cursor = db.query("SELECT DISTINCT $col FROM $tableName WHERE $col IS NOT NULL") - cursor.use { - buildList { - while (it.moveToNext()) { - add(it.getString(0)) + override fun distinctValues(fieldName: String): Sequence = runBlocking { + ifOpen(emptySequence()) { + val col = fieldColumns[fieldName] + ?: throw IllegalArgumentException("Unknown field: $fieldName") + val cursor = db.query("SELECT DISTINCT $col FROM $tableName WHERE $col IS NOT NULL") + cursor.use { + buildList { + while (it.moveToNext()) { + add(it.getString(0)) + } } - } - }.asSequence() + }.asSequence() + } } override suspend fun insertAll(entries: Sequence) = withContext(Dispatchers.IO) { @@ -185,19 +189,20 @@ class SQLiteIndex( } override fun close() { - lock.write { - if (closed) return - closed = true - db.close() + runBlocking { + mutex.withLock { + if (closed) return@withLock + closed = true + db.close() + } } } - private inline fun ifOpen(default: R, block: () -> R): R = - lock.read { if (closed) default else block() } + private suspend inline fun ifOpen(default: R, crossinline block: () -> R): R = + mutex.withLock { if (closed) default else block() } - private inline fun ifOpen(block: () -> Unit) { - lock.read { if (!closed) block() } - } + private suspend inline fun ifOpen(crossinline block: () -> Unit) = + mutex.withLock { if (!closed) block() } suspend fun size(): Int = withContext(Dispatchers.IO) { ifOpen(0) { diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt index 570e1aa624..a61a86f84b 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/util/BackgroundIndexer.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeoutOrNull import org.appdevforall.codeonthego.indexing.api.Index import org.appdevforall.codeonthego.indexing.api.Indexable import org.slf4j.LoggerFactory @@ -53,7 +52,6 @@ class BackgroundIndexer( companion object { private val log = LoggerFactory.getLogger(BackgroundIndexer::class.java) - private const val CLOSE_TIMEOUT_MS = 5_000L } var progressListener: IndexingProgressListener? = null @@ -167,14 +165,15 @@ class BackgroundIndexer( val activeJobCount: Int get() = activeJobs.size override fun close() { + val activeCount = activeJobCount + if (activeCount > 0) { + log.warn( + "Closing indexer with {} active job(s); cancellation is cooperative and close will wait for completion", + activeCount, + ) + } runBlocking { - val completed = withTimeoutOrNull(CLOSE_TIMEOUT_MS) { - job.cancelAndJoin() - } - if (completed == null) { - log.warn("Indexer close timed out after {}ms, force-cancelling", CLOSE_TIMEOUT_MS) - job.cancel() - } + job.cancelAndJoin() } activeJobs.clear() } From 9163016fd0187f31c1e5c9aa5cfa0c612c8045c1 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Sat, 25 Apr 2026 12:58:58 -0700 Subject: [PATCH 5/6] Rename insertBatch to insertBatchLocked --- .../org/appdevforall/codeonthego/indexing/SQLiteIndex.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt index cce8760da4..354164e413 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt @@ -167,17 +167,17 @@ class SQLiteIndex( for (entry in entries) { batch.add(entry) if (batch.size >= batchSize) { - ifOpen { insertBatch(batch) } + ifOpen { insertBatchLocked(batch) } batch.clear() } } if (batch.isNotEmpty()) { - ifOpen { insertBatch(batch) } + ifOpen { insertBatchLocked(batch) } } } override suspend fun insert(entry: T) = withContext(Dispatchers.IO) { - ifOpen { insertBatch(listOf(entry)) } + ifOpen { insertBatchLocked(listOf(entry)) } } override suspend fun removeBySource(sourceId: String) = withContext(Dispatchers.IO) { @@ -251,7 +251,7 @@ class SQLiteIndex( } } - private fun insertBatch(entries: List) { + private fun insertBatchLocked(entries: List) { db.beginTransaction() try { for (entry in entries) { From 9376c6f626e110ef099577db42b526dfa11f9789 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Sat, 25 Apr 2026 13:03:53 -0700 Subject: [PATCH 6/6] Add warning when SQLiteIndex.close() called on the main thread --- .../appdevforall/codeonthego/indexing/SQLiteIndex.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt index 354164e413..4d8dae0627 100644 --- a/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt +++ b/lsp/indexing/src/main/kotlin/org/appdevforall/codeonthego/indexing/SQLiteIndex.kt @@ -3,6 +3,7 @@ package org.appdevforall.codeonthego.indexing import android.content.ContentValues import android.content.Context import android.database.sqlite.SQLiteDatabase +import android.os.Looper import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory @@ -15,6 +16,7 @@ import org.appdevforall.codeonthego.indexing.api.Index import org.appdevforall.codeonthego.indexing.api.IndexDescriptor import org.appdevforall.codeonthego.indexing.api.IndexQuery import org.appdevforall.codeonthego.indexing.api.Indexable +import org.slf4j.LoggerFactory import kotlin.collections.iterator /** @@ -61,6 +63,10 @@ class SQLiteIndex( override val name: String = "sqlite:${descriptor.name}", private val batchSize: Int = 500, ) : Index { + companion object { + private val log = LoggerFactory.getLogger(SQLiteIndex::class.java) + } + private val tableName = descriptor.name.replace(Regex("[^a-zA-Z0-9_]"), "_") @@ -189,6 +195,11 @@ class SQLiteIndex( } override fun close() { + if (Looper.getMainLooper() == Looper.myLooper()) { + log.warn( + "SQLiteIndex.close() called on the main thread; waiting on mutex and closing db may block and cause ANR" + ) + } runBlocking { mutex.withLock { if (closed) return@withLock