fix(mcp): устранить дедлок analyze_file на .os-классах Autumn#4273
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthrough
ChangesMCP document analysis concurrency
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant McpToolsTest
participant McpDocumentReader
participant diagnosticComputerExecutor
participant TypeService
McpToolsTest->>McpDocumentReader: analyze document
McpDocumentReader->>diagnosticComputerExecutor: submit expression type computation
diagnosticComputerExecutor->>TypeService: expressionTypesAt
TypeService-->>diagnosticComputerExecutor: return types
diagnosticComputerExecutor-->>McpDocumentReader: complete callback
McpDocumentReader-->>McpToolsTest: return analysis result
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/main/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpDocumentReader.java (2)
111-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting the build/downgrade/cleanup logic into a helper method.
The four nested try/finally levels (writeLock build → downgrade → action → writeLock cleanup) are correct but hard to scan in one method. Extracting the "fresh AST" branch into a private helper (e.g.
buildAndAnalyze(...), with cleanup in its ownclearDocumentQuietly(...)) would make the lock lifecycle easier to verify at a glance.♻️ Suggested extraction
private <T> T buildAndAnalyze(ServerContext serverContext, ReadWriteLock lock, URI uri, Function<DocumentContext, T> action) { lock.writeLock().lock(); var writeHeld = true; try (var ignored = WorkspaceContextHolder.forUri(serverContext.getWorkspaceUri())) { var documentContext = serverContext.addDocument(uri); serverContext.rebuildDocument(documentContext); lock.readLock().lock(); lock.writeLock().unlock(); writeHeld = false; try { return action.apply(documentContext); } finally { lock.readLock().unlock(); clearDocumentQuietly(serverContext, lock, documentContext); } } finally { if (writeHeld) { lock.writeLock().unlock(); } } } private void clearDocumentQuietly(ServerContext serverContext, ReadWriteLock lock, DocumentContext documentContext) { lock.writeLock().lock(); try { serverContext.tryClearDocument(documentContext); } finally { lock.writeLock().unlock(); } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpDocumentReader.java` around lines 111 - 147, Extract the fresh-AST build, lock downgrade, action execution, and cleanup from the current method into a private helper such as buildAndAnalyze, preserving its lock and WorkspaceContextHolder lifecycle. Move the write-locked tryClearDocument operation into a separate clearDocumentQuietly helper, and have the existing method delegate to the new helper without changing behavior.
133-141: 🚀 Performance & Scalability | 🔵 TrivialCleanup re-acquires
writeLocksynchronously before returning to the caller.
tryClearDocumentruns in thefinallybeforeaction.apply()'s result is actually returned, so the calling thread (likely the MCP request thread) blocks onwriteLock().lock()if there's read contention on the same document, adding latency to the response path even though the computed result is already available. This is strictly better than the prior always-writeLocked design, but if cleanup latency ever becomes noticeable under concurrent load, consider offloading it to a background executor instead of blocking the caller.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpDocumentReader.java` around lines 133 - 141, In the cleanup block of McpDocumentReader, avoid synchronously reacquiring writeLock before returning action.apply()’s result. Offload serverContext.tryClearDocument(documentContext) to the existing background executor or an appropriate asynchronous cleanup mechanism, while preserving best-effort cleanup and ensuring the caller returns the computed result without waiting for lock contention.src/test/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpToolsTest.java (1)
142-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStrengthen the final assertion to actually validate type inference, not just deadlock-freedom.
TypeService.expressionTypesAtfalls back to the non-nullTypeSet.EMPTYsentinel when no expression is found at the given position, soassertThat(types).isNotNull()would still pass even if the line/position lookup silently missed the target expression. Given the PR description calls out validating the inferred&Пластилинtype, consider asserting on the actual contents (e.g. non-empty, or containing the expected type name). Also, the line-search loop leavesline == -1if"Логгер.Записать"isn't found, which surfaces as an opaqueArrayIndexOutOfBoundsExceptionatlines[line]rather than a clear failure message.♻️ Suggested strengthening
- var position = new Position(line, lines[line].indexOf("Логгер") + 1); + assertThat(line).as("fixture line containing 'Логгер.Записать' not found").isNotEqualTo(-1); + var position = new Position(line, lines[line].indexOf("Логгер") + 1);- assertThat(types).isNotNull(); + assertThat(types).isNotNull(); + assertThat(types.isEmpty()).isFalse();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpToolsTest.java` around lines 142 - 165, Strengthen the test around the expressionTypesAt call by making the line lookup fail clearly when "Логгер.Записать" is absent, rather than indexing with the initial -1 value. Replace the final non-null assertion on types with an assertion that verifies the inferred result is non-empty and contains the expected "&Пластилин" type name, while preserving the existing timeout and interruption handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpDocumentReader.java`:
- Around line 111-147: Extract the fresh-AST build, lock downgrade, action
execution, and cleanup from the current method into a private helper such as
buildAndAnalyze, preserving its lock and WorkspaceContextHolder lifecycle. Move
the write-locked tryClearDocument operation into a separate clearDocumentQuietly
helper, and have the existing method delegate to the new helper without changing
behavior.
- Around line 133-141: In the cleanup block of McpDocumentReader, avoid
synchronously reacquiring writeLock before returning action.apply()’s result.
Offload serverContext.tryClearDocument(documentContext) to the existing
background executor or an appropriate asynchronous cleanup mechanism, while
preserving best-effort cleanup and ensuring the caller returns the computed
result without waiting for lock contention.
In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpToolsTest.java`:
- Around line 142-165: Strengthen the test around the expressionTypesAt call by
making the line lookup fail clearly when "Логгер.Записать" is absent, rather
than indexing with the initial -1 value. Replace the final non-null assertion on
types with an assertion that verifies the inferred result is non-empty and
contains the expected "&Пластилин" type name, while preserving the existing
timeout and interruption handling.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 41e8f6d2-2fee-4714-b5d5-05dcda59ed01
⛔ Files ignored due to path filters (3)
src/test/resources/mcp/autumn-deadlock/lib.configis excluded by!src/test/resources/**src/test/resources/mcp/autumn-deadlock/src/Логгер.osis excluded by!src/test/resources/**src/test/resources/mcp/autumn-deadlock/src/Приложение.osis excluded by!src/test/resources/**
📒 Files selected for processing (2)
src/main/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpDocumentReader.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpToolsTest.java
798e2d3 to
52de1b1
Compare
analyze_file держал writeLock документа на весь getDiagnostics, а ленивая сборка Autumn-индекса (запускается выводом типа внедрённого &Пластилин-бина на пуле diagnostic-computer) реентрантно берёт readLock того же документа через ServerContext.getDocument. Под чужим writeLock это вечный дедлок: поток запроса ждёт результат диагностик, а поток-сборщик индекса — readLock, который как write удерживает поток запроса. McpDocumentReader.access теперь понижает блокировку до readLock на время выполнения action (вычисление диагностик — read-операция относительно документного RWLock; ветка read-доступа выше уже так делает), возвращая writeLock лишь для очистки транзиентного AST. Read-доступ совместим с реентрантным readLock сборщика индекса — дедлок исключён. Добавлен регресс-тест, детерминированно воспроизводящий дедлок через кросс-поточный вывод типа &Пластилин-бина под analyze (на том же потоке readLock переиспользовал бы writeLock и баг бы не проявился). Co-Authored-By: Claude Opus 4.8 <[email protected]>
52de1b1 to
5916155
Compare
|



Проблема
MCP-инструмент
analyze_fileнамертво виснет (не долгий анализ, а дедлок) на OneScript-классах фреймворка «ОСень» (&Пластилин-внедрение). Диагностировано по thread dump живого процесса: все потокиdiagnostic-computer-*вWAITING (parking)с нулевым CPU.Первопричина
McpDocumentReader.access(ветка свежего AST дляanalyze) держит writeLock документа на весьgetDiagnostics(). Диагностики считаются на пулеdiagnostic-computer; вывод типа внедрённого&Пластилин-бина запускает ленивую сборку Autumn-индекса (AbstractOScriptLazyIndex.ensureBuilt→AbstractAutumnLibraryIndex.rebuild), которая реентрантно берёт readLock того же документа черезServerContext.getDocument.Цикл ожидания (ABBA на per-URI
ReentrantReadWriteLock):writeLock(doc)и ждёт результат диагностик наfuture.get();readLock(doc), который как write удерживает поток запроса.Конфликт возникает только на URI самого анализируемого файла (
getDocumentLock— per-URI), когда он сам является классом Autumn-библиотеки и Autumn-индекс на момент анализа холодный.Решение
McpDocumentReader.accessпонижает блокировкуwriteLock → readLockна время выполненияaction: AST строится и очищается подwriteLock, но сам анализ идёт подreadLock. Обоснование — вычисление диагностик уже является read-операцией относительно документного RWLock: ветка read-доступа этого же метода (для открытых/проиндексированных документов) выполняетactionровно подreadLock. Read-доступ совместим с реентрантнымreadLockсборщика индекса, поэтому весь класс таких дедлоков устраняется, а не только Autumn.Понижение сделано без окна (readLock берётся до отпускания writeLock),
writeLockповторно берётся лишь для очистки транзиентного AST.Тест
Добавлен детерминированный регресс-тест
McpToolsTest.analyzeDoesNotDeadlockWhenActionTriggersAutumnIndexBuild+ минимальная фикстураsrc/test/resources/mcp/autumn-deadlock/. Тест форкает вывод типа&Пластилин-бина на пулdiagnosticComputerExecutorподanalyze(кросс-поточность обязательна — на потоке-владельце writeLock readLock переиспользовался бы и баг не проявился). Проверено: FAILED (timeout) на develop → PASSED с фиксом. Весь пакет тестовmcp.*зелёный.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests