fix(storage-ksqlite): deadlock in ConversationStore.delete/rename
ci / JVM build + tests (push) Failing after 1m16s

mutex в kotlinx.coroutines НЕ reentrant — при вызове get() изнутри withLock
получаем deadlock. ConversationStore.delete() и rename() использовали именно
этот паттерн для проверки существования.

Fixed: заменил на raw SELECT 1 FROM conversation WHERE id=? и
SELECT updated_at FROM conversation WHERE id=? — те же проверки, без
повторного взятия mutex.

Discovered by full-test-suite run: 3 tests in KsqliteConversationStoreTest
были в UncompletedCoroutinesError (UncompletedCoroutinesError после 1 минуты
ожидания), хотя отдельный прогон EventStore (где нет вызовов get() внутри
mutex) проходил. После фикса все 36 тестов проходят:
  - KsqliteEventStoreTest         (11)
  - KsqliteConversationStoreTest  (9)
  - KsqliteMessageStoreTest       (5)
  - KsqliteWorkingMemoryStoreTest (5)
  - KsqliteReflectionStoreTest    (6)
This commit is contained in:
2026-09-20 14:38:48 +03:00
parent bf2649a856
commit dd7aec8df1
@@ -70,8 +70,12 @@ class KsqliteConversationStore(
override suspend fun delete(id: String): Boolean = withContext(Dispatchers.Default) {
mutex.withLock {
val existed = get(id) != null
if (!existed) return@withContext false
// Проверяем существование через raw query, НЕ через get() — get() тоже
// берёт mutex (не реентрант), что привело бы к deadlock.
connection.prepare("SELECT 1 FROM conversation WHERE id = ?").use { check ->
check.bindText(1, id)
check.executeQuery().use { rs -> if (!rs.next()) return@withContext false }
}
messageStore?.clear(id)
workingMemoryStore?.clear(id)
connection.prepare("DELETE FROM conversation WHERE id = ?").use { stmt ->
@@ -108,7 +112,13 @@ class KsqliteConversationStore(
stmt.bindText(3, id)
stmt.executeUpdate()
}
get(id)?.updatedAt
// raw query instead of get() (deadlock — get() also takes mutex)
connection.prepare("SELECT updated_at FROM conversation WHERE id = ?").use { stmt ->
stmt.bindText(1, id)
stmt.executeQuery().use { rs ->
if (rs.next()) Instant.fromEpochMilliseconds(rs.getLong(0)!!) else null
}
}
}
}