Repository navigation
fix(core): resolve read-lock deadlock and harden config loading - #1491
Conversation
- Wrap FileEditorManager and ProjectFileIndex access in readSync so config reloads on background threads hold a proper read action - Replace runBlocking inside the field.order.with comparator with a stable suspending merge sort (suspendMergeSort), so rule scripts no longer block a thread that holds the read lock - Isolate config source failures in LayeredConfigReader so one failing source no longer discards entries from the others, and log the failed source - Add unit tests for suspendMergeSort
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1491 +/- ##
=============================================
+ Coverage 69.934% 69.975% +0.041%
=============================================
Files 518 518
Lines 30383 30398 +15
Branches 7147 7153 +6
=============================================
+ Hits 21248 21271 +23
+ Misses 6374 6370 -4
+ Partials 2761 2757 -4
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 3 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
|
@alona789 thanks for tracking down the IDE freeze! the suspending sort looks like the right fix, and all five sort tests passed locally. Before merge: please rethrow Non-blocking suggestions:
|
|
A PR (#1495) has been opened to fix this issue. You can download the packaged plugin from the GitHub Actions workflow run by clicking on the "Artifacts" dropdown:
Each artifact is a ready-to-install plugin zip — pick the one matching your IDEA version and install it via Settings → Plugins → ⚙ → Install Plugin from Disk. No extraction needed. |
* fix(core): propagate config reload cancellation
Editing config files in quick succession could leave the plugin
running with a partial config: a superseded reload still published
the sources it had managed to read and notified listeners, so rules
from the remaining sources briefly disappeared.
Root cause: `LayeredConfigReader.reload()` wraps each source in
`runCatching` to isolate failures. A `CancellationException` raised
while waiting on an IntelliJ read action was logged as an ordinary
source failure, so the cancelled reload ran to completion.
`DefaultConfigReader.reload()` then replaced the active delegate and
fired `ConfigReloadListener` without checking whether its coroutine
was still active. The remote config loader can hide the cancellation
entirely: it catches it and returns a fallback.
Solution: rethrow `CancellationException` from the per-source
`onFailure` while keeping warn-and-skip for real source failures,
and call `currentCoroutineContext().ensureActive()` after the new
delegate has loaded but before it becomes active and listeners are
notified. Add tests for both paths plus a guard that ordinary
failures are still isolated. Both guards were reverse-verified:
removing either one makes the new tests fail.
* refactor(psi): extract suspendMergeSort
Move the suspending merge sort out of `DefaultPsiClassHelper.kt` into
a sibling `SuspendMergeSort.kt`, keeping it `internal`. It has no PSI
dependency and already has its own tests. Translate its KDoc and the
comparator test comment to English, matching the rest of the
codebase.
* test(config): move module_path tests to a fixture
The three `${module_path}` cases call `readSync`, which needs an
IntelliJ application. They only passed when another fixture test had
already set one up, so a focused run of `ConfigTextParserTest` failed.
Move them to `ConfigTextParserModulePathTest`, based on the project's
light IDE fixture, so they pass in isolation too.
* docs(psi): translate field ordering comments
Translate the last three Chinese comments added by #1491 at the
`suspendMergeSort` call site. All six Chinese comment lines from that
PR are now English.
Impact: ordinary source-failure isolation is unchanged. Apart from
the two cancellation guards, this branch is test, refactor and
comment only. The changes above address the review on #1491: both
"Before merge" requests and both non-blocking suggestions. Verified
with `./gradlew cleanTest test`: 7669 tests, 0 failures, 0 errors,
10 skipped.
Refs: #1491
* fix(core): propagate config reload cancellation
Editing config files in quick succession could leave the plugin
running with a partial config: a superseded reload still published
the sources it had managed to read and notified listeners, so rules
from the remaining sources briefly disappeared.
Root cause: `LayeredConfigReader.reload()` wraps each source in
`runCatching` to isolate failures. A `CancellationException` raised
while waiting on an IntelliJ read action was logged as an ordinary
source failure, so the cancelled reload ran to completion.
`DefaultConfigReader.reload()` then replaced the active delegate and
fired `ConfigReloadListener` without checking whether its coroutine
was still active. The remote config loader can hide the cancellation
entirely: it catches it and returns a fallback.
Solution: rethrow `CancellationException` from the per-source
`onFailure` while keeping warn-and-skip for real source failures,
and call `currentCoroutineContext().ensureActive()` after the new
delegate has loaded but before it becomes active and listeners are
notified. Add tests for both paths plus a guard that ordinary
failures are still isolated. Both guards were reverse-verified:
removing either one makes the new tests fail.
* refactor(psi): extract suspendMergeSort
Move the suspending merge sort out of `DefaultPsiClassHelper.kt` into
a sibling `SuspendMergeSort.kt`, keeping it `internal`. It has no PSI
dependency and already has its own tests. Translate its KDoc and the
comparator test comment to English, matching the rest of the
codebase.
* test(config): move module_path tests to a fixture
The three `${module_path}` cases call `readSync`, which needs an
IntelliJ application. They only passed when another fixture test had
already set one up, so a focused run of `ConfigTextParserTest` failed.
Move them to `ConfigTextParserModulePathTest`, based on the project's
light IDE fixture, so they pass in isolation too.
* docs(psi): translate field ordering comments
Translate the last three Chinese comments added by tangcent/easy-yapi#1491 at the
`suspendMergeSort` call site. All six Chinese comment lines from that
PR are now English.
Impact: ordinary source-failure isolation is unchanged. Apart from
the two cancellation guards, this branch is test, refactor and
comment only. The changes above address the review on tangcent/easy-yapi#1491: both
"Before merge" requests and both non-blocking suggestions. Verified
with `./gradlew cleanTest test`: 7669 tests, 0 failures, 0 errors,
10 skipped.
Refs: tangcent/easy-yapi#1491
Description
Fix an IDE freeze caused by a read-lock deadlock in field ordering, and make config loading more robust.
When
field.order.withis configured,DefaultPsiClassHelper.applyFieldOrderingcalledrunBlockinginside thesortedWithcomparator. The calling thread already held a read action, and the rule script it waited for (Groovyit.name()→readSync) had to acquire the read permit again. Once a write action was pending, new read permits were blocked, so the read-lock holder, the script coroutine and the pending writer waited on each other. The EDT then hung inrunReadAction/runWriteActionfor 30+ seconds.Config reloads on the plugin's background thread also read
FileEditorManagerandProjectFileIndexwithout a read action, and a single failing config source discarded the entries of every other source.Type of Change
Bug fix (non-breaking change which fixes an issue)
New feature (non-breaking change which adds functionality)
Breaking change (fix or feature that would cause existing functionality to not work as expected)
Refactoring (no functional changes)
Documentation update
Performance improvement
Test coverage improvement
Related Issues
Fixes #
Affected Area
YApi export
Postman export
Markdown export
Send HTTP request (Call)
API scanning / dashboard
Rule engine / custom rules / config
Settings / UI
Other / not user-facing
Changes Made
DefaultPsiClassHelper: removerunBlockingfrom thefield.order.withcomparator and sort with the newsuspendMergeSort, a stable merge sort that accepts a suspending comparator. Rule evaluation now suspends instead of blocking the thread that holds the read lock. An inconsistent user comparator no longer throwsComparison method violates its general contract.ConfigTextParser.activeModuleContentRoot: wrapFileEditorManager.selectedFilesandProjectFileIndexaccess inreadSync.LayeredConfigReader: catch failures per config source, log the failed source, and keep loading the remaining sources.SuspendMergeSortTest(ordering, empty/single element, stability, suspending comparator on another dispatcher, inconsistent comparator).How I Tested
./gradlew testpassesManual verification in a sandbox IDE (
./gradlew runIde) — describe the entry action exercisedNew tests added for new functionality
Root cause was identified from IDE freeze thread dumps:
EasyAPI-backgroundblocked inDefaultPsiClassHelper.applyFieldOrdering$lambda$22→runBlockingwhile the EDT waited on a read permit withwriteActionPending:true.Architecture & Threading Checklist
New code lives in the right bucket (
core/psi,core/config, existing files); no newcore.*→ concrete sibling importsPSI/VFS reads wrapped in
read {}/readSync {}—ConfigTextParsernow usesreadSyncfor editor and file index access; the sort itself performs no PSI readsBoundary classes self-protect every public member — N/A, no boundary class touched
PSI writes and UI updates go through
write {}/swing {}— N/A, no writes or UINo new
launch(Dispatchers.Default)from EDT / startup contexts — none added; the sort stays suspending and no longer blocks a threadLogging Checklist
Exactly one channel per event — a failed config source is reported once through
LOG.warnNo
LOG.error,LOG.debug,LOG.trace,println, orprintStackTrace()in production codeThrowables passed as the last argument; no silent
runCatching{}.getOrNull()or emptycatch— therunCatchinginLayeredConfigReaderlogs the failure with the throwableGeneral Checklist
My code follows the project's architecture principles
I have performed a self-review of my code
I have commented my code, particularly in hard-to-understand areas
I have made corresponding changes to the documentation
My changes generate no new warnings
I have added tests that prove my fix is effective or that my feature works
New and existing unit tests pass locally with my changes
Screenshots (if applicable)
N/A
Additional Notes
runBlockingusage exists undersrc/main, so this was the only occurrence.BuiltInExtensionFieldOrderScenarioTeststill coversfield.order.withend to end.