Skip to content

fix(mcp): устранить дедлок analyze_file на .os-классах Autumn#4273

Merged
nixel2007 merged 1 commit into
developfrom
fix/mcp-analyze-file-autumn-deadlock
Jul 15, 2026
Merged

fix(mcp): устранить дедлок analyze_file на .os-классах Autumn#4273
nixel2007 merged 1 commit into
developfrom
fix/mcp-analyze-file-autumn-deadlock

Conversation

@sfaqer

@sfaqer sfaqer commented Jul 15, 2026

Copy link
Copy Markdown
Member

Проблема

MCP-инструмент analyze_file намертво виснет (не долгий анализ, а дедлок) на OneScript-классах фреймворка «ОСень» (&Пластилин-внедрение). Диагностировано по thread dump живого процесса: все потоки diagnostic-computer-* в WAITING (parking) с нулевым CPU.

Первопричина

McpDocumentReader.access (ветка свежего AST для analyze) держит writeLock документа на весь getDiagnostics(). Диагностики считаются на пуле diagnostic-computer; вывод типа внедрённого &Пластилин-бина запускает ленивую сборку Autumn-индекса (AbstractOScriptLazyIndex.ensureBuiltAbstractAutumnLibraryIndex.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

    • Improved concurrency and locking during document analysis to prevent deadlocks when analysis triggers additional indexing and type computations.
    • Enhanced robustness of post-analysis cleanup to minimize the chance of stale state after failures.
  • Tests

    • Added a regression test that runs document analysis under concurrent type computation and verifies results (including a resolved reference) complete within a specified time limit.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 82004f7d-3151-4ea3-81c1-8532208c0b3d

📥 Commits

Reviewing files that changed from the base of the PR and between 52de1b1 and 5916155.

⛔ Files ignored due to path filters (3)
  • src/test/resources/mcp/autumn-deadlock/lib.config is excluded by !src/test/resources/**
  • src/test/resources/mcp/autumn-deadlock/src/Логгер.os is excluded by !src/test/resources/**
  • src/test/resources/mcp/autumn-deadlock/src/Приложение.os is excluded by !src/test/resources/**
📒 Files selected for processing (2)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpDocumentReader.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpToolsTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpDocumentReader.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpToolsTest.java

📝 Walkthrough

Walkthrough

McpDocumentReader now downgrades its lock before running analysis actions and clears temporary AST state afterward. McpToolsTest adds a regression test for callbacks that trigger concurrent type computation.

Changes

MCP document analysis concurrency

Layer / File(s) Summary
Document access lock lifecycle
src/main/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpDocumentReader.java
AST rebuilding occurs under the write lock, callbacks run under the read lock, and document cleanup occurs in a subsequent write-lock phase.
Concurrent analysis regression coverage
src/test/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpToolsTest.java
Injected analysis services and a timeout-based test cover callbacks that trigger Autumn index construction and expression type computation.

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly matches the main change: fixing an analyze_file deadlock on Autumn .os classes in MCP.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-analyze-file-autumn-deadlock

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/main/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpDocumentReader.java (2)

111-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider 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 own clearDocumentQuietly(...)) 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 | 🔵 Trivial

Cleanup re-acquires writeLock synchronously before returning to the caller.

tryClearDocument runs in the finally before action.apply()'s result is actually returned, so the calling thread (likely the MCP request thread) blocks on writeLock().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 win

Strengthen the final assertion to actually validate type inference, not just deadlock-freedom.

TypeService.expressionTypesAt falls back to the non-null TypeSet.EMPTY sentinel when no expression is found at the given position, so assertThat(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 leaves line == -1 if "Логгер.Записать" isn't found, which surfaces as an opaque ArrayIndexOutOfBoundsException at lines[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

📥 Commits

Reviewing files that changed from the base of the PR and between abac190 and 798e2d3.

⛔ Files ignored due to path filters (3)
  • src/test/resources/mcp/autumn-deadlock/lib.config is excluded by !src/test/resources/**
  • src/test/resources/mcp/autumn-deadlock/src/Логгер.os is excluded by !src/test/resources/**
  • src/test/resources/mcp/autumn-deadlock/src/Приложение.os is excluded by !src/test/resources/**
📒 Files selected for processing (2)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpDocumentReader.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/mcp/McpToolsTest.java

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Test Results

 3 636 files  ±0   3 636 suites  ±0   1h 49m 47s ⏱️ - 1m 35s
 3 594 tests +1   3 576 ✅ +1   18 💤 ±0  0 ❌ ±0 
21 564 runs  +6  21 452 ✅ +6  112 💤 ±0  0 ❌ ±0 

Results for commit 5916155. ± Comparison against base commit abac190.

♻️ This comment has been updated with latest results.

@sfaqer
sfaqer force-pushed the fix/mcp-analyze-file-autumn-deadlock branch from 798e2d3 to 52de1b1 Compare July 15, 2026 04:08
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]>
@sfaqer
sfaqer force-pushed the fix/mcp-analyze-file-autumn-deadlock branch from 52de1b1 to 5916155 Compare July 15, 2026 04:32
@sonarqubecloud

Copy link
Copy Markdown

@nixel2007
nixel2007 merged commit 6419ab1 into develop Jul 15, 2026
37 checks passed
@nixel2007
nixel2007 deleted the fix/mcp-analyze-file-autumn-deadlock branch July 15, 2026 05:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants