From dd7aec8df1b5c983508886b3e761c999b465f778 Mon Sep 17 00:00:00 2001 From: subochev Date: Sun, 20 Sep 2026 14:38:48 +0300 Subject: [PATCH] fix(storage-ksqlite): deadlock in ConversationStore.delete/rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../storage/ksqlite/KsqliteConversationStore.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteConversationStore.kt b/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteConversationStore.kt index 51ef24d..ae6520a 100644 --- a/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteConversationStore.kt +++ b/storage-ksqlite/src/commonMain/kotlin/pw/binom/agentik/storage/ksqlite/KsqliteConversationStore.kt @@ -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 + } + } } }