ADFA-4387: add global analysis lock for Kotlin analysis#1428
ADFA-4387: add global analysis lock for Kotlin analysis#1428itsaky-adfa wants to merge 7 commits into
Conversation
Signed-off-by: Akash Yadav <itsaky01@gmail.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 Walkthrough
WalkthroughAdds serialized Kotlin Analysis API access, updates completions and diagnostics to use the new analysis path, moves modification handling under the write lock, and adds regression tests for concurrent analysis. ChangesKotlin analysis concurrency and diagnostic flow
Sequence Diagram(s)sequenceDiagram
participant withAnalysisLock
participant KotlinCompletions
participant KotlinDiagnosticProvider
participant AddImportAction
participant CompilationEnvironment
KotlinCompletions->>withAnalysisLock: analyzeMaybeDangling(completionKtFile)
withAnalysisLock-->>KotlinCompletions: completion context
KotlinDiagnosticProvider-->>AddImportAction: KotlinDiagnosticExtra(unresolvedReference, env)
AddImportAction->>AddImportAction: findSymbolBySimpleName(reference)
CompilationEnvironment->>CompilationEnvironment: project.read / project.write
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 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.
Actionable comments posted: 1
🤖 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.
Inline comments:
In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt`:
- Around line 150-153: Move the withAnalysisLock block outside and after the
env.project.read section to avoid deadlock risks from nested lock acquisition.
Since completionKtFile is already created in the earlier project.read block
(lines 137-145) and its content is stable, remove the second env.project.read
block entirely. Keep only the withAnalysisLock call wrapping the analysis work
that follows, ensuring the analysis lock is acquired after the project read lock
is released rather than held concurrently.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 491ec53b-f447-412a-86a2-ec1c9d81839e
📒 Files selected for processing (2)
lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.ktlsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt
Signed-off-by: Akash Yadav <itsaky01@gmail.com>
hal-eisen-adfa
left a comment
There was a problem hiding this comment.
Automated high-effort review (code-review, recall-biased). The global analysisLock is a reasonable fix for the reported crash — verifier-confirmed root cause is concurrent analyze from indexing + diagnostics running under the shared project.read (the RW lock allows multiple readers), so serializing analyze closes that gap.
However, two confirmed, still-open instances of the same KaInaccessibleLifetimeOwnerAccessException class are not fixed by this PR. They're in files this PR doesn't touch, so I couldn't attach them inline — flagging here:
🟥 1. Escaped lifetime-owner: diagnostics → AddImportAction (CONFIRMED)
KotlinDiagnosticProvider.kt:~83 stores the live KaDiagnosticWithPsi (a KaLifetimeOwner) into DiagnosticItem.extra (KotlinDiagnosticExtra(diagnostic, env)) instead of extracting primitives inside the analyze block. Later AddImportAction.prepare() (AddImportAction.kt:56) and execAction() (:80) read diagnostic.reference outside any analyze block. prepare() runs whenever the code-actions menu is built for a Kotlin file with an unresolved-reference diagnostic (action is registered via KotlinCodeActionsMenu → KotlinLanguageServer). A lock can't keep an escaped token valid → same exception, from the quick-fix path. Fix: extract the reference string/FqName (plain data) inside the analyze block; don't store Ka* lifetime owners in extra.
🟥 2. handleElementModification outside the write lock (CONFIRMED)
CompilationEnvironment.kt:218-221 (notifyElementModifiedForPath, used by onFileCreated/onFileRemoved) calls KaSourceModificationService.handleElementModification(...) before the project.write { block opens at line 226. That session mutation is serialized by neither the RW lock nor the new analysisLock, so it can race a concurrent analyze (which holds only project.read) → the same lifetime/session corruption. (onFileContentChanged at :283 correctly keeps it inside project.write.) Fix: move the call inside project.write {} (or take analysisLock around it).
If the goal is to eliminate this exception class (not just the indexing-vs-diagnostics race), #1 and #2 should be addressed too. The inline comments below cover responsiveness/altitude/cleanup on the changed lines.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt (1)
15-31: ⚡ Quick winConsider adding a test for lock reentrancy.
The KDoc in
KtFileExts.ktnotes thatanalysisLockis aReentrantLockspecifically so "an (indirect) nested analysis on the same thread cannot deadlock." While these tests validate mutual exclusion and lifetime exception prevention, they don't verify that nested or reentrant analysis calls work correctly.If the lock were later changed to a non-reentrant implementation, these tests would still pass but nested analysis scenarios would deadlock.
Example test structure
`@Test` fun `nested analyzeMaybeDangling calls do not deadlock`(): Unit = runBlocking { val file = createSourceFile("Nested.kt", "class Outer { class Inner }") env.project.read { analyzeMaybeDangling(file) { // Outer analysis file.declarations.first().symbol // Nested analysis on same thread analyzeMaybeDangling(file) { file.declarations.first().symbol } } } // If lock is non-reentrant, this would deadlock }🤖 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 `@lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt` around lines 15 - 31, The AnalysisSerializationTest class currently validates mutual exclusion and lifetime exception prevention but does not test that nested or reentrant analyzeMaybeDangling calls work correctly without deadlocking. Add a new test method to the AnalysisSerializationTest class that verifies reentrancy by calling analyzeMaybeDangling from within another analyzeMaybeDangling block on the same thread, ensuring the ReentrantLock behavior is preserved. This test should complete without deadlock and demonstrate that the analysisLock reentrant implementation allows nested analysis calls to proceed safely.
🤖 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
`@lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt`:
- Around line 15-31: The AnalysisSerializationTest class currently validates
mutual exclusion and lifetime exception prevention but does not test that nested
or reentrant analyzeMaybeDangling calls work correctly without deadlocking. Add
a new test method to the AnalysisSerializationTest class that verifies
reentrancy by calling analyzeMaybeDangling from within another
analyzeMaybeDangling block on the same thread, ensuring the ReentrantLock
behavior is preserved. This test should complete without deadlock and
demonstrate that the analysisLock reentrant implementation allows nested
analysis calls to proceed safely.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: eb359812-c8e8-4308-87b4-6ec25f69befd
📒 Files selected for processing (1)
lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt
… completion cleanup) Apply the actionable items from Hal's review on PR #1428: - Diagnostics: extract the unresolved-reference name inside the analyze block instead of storing the live KaDiagnosticWithPsi (a KaLifetimeOwner) in DiagnosticItem.extra. AddImportAction now reads the pre-extracted string, preventing KaInaccessibleLifetimeOwnerAccessException from the quick-fix path. - CompilationEnvironment.notifyElementModifiedForPath: run handleElementModification inside project.write so the session mutation can't race a concurrent analyze (mirrors onFileContentChanged). - KotlinCompletions: collapse manual withAnalysisLock + analyzeCopy into analyzeMaybeDangling, removing the only in-prod direct analyzeCopy call. - KtFileExts: document that code under withAnalysisLock must not call project.write (non-upgradeable RW lock footgun).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In
`@lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt`:
- Around line 213-219: The PSI and module resolution calls in the code block
(involving psiManager.findFile, structureProvider.getModule, and
findModuleForSourceId) are executing without read lock protection, creating a
race condition vulnerability since concurrent analyze operations hold read
locks. Wrap the entire code block from structureProvider.getInstance through the
module assignment in a project.read block, following the same pattern used in
the loadKtFile method which correctly protects its psiManager.findFile call with
project.read.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0ac09d2a-2484-41fe-af4b-6bcd7daa70e1
📒 Files selected for processing (5)
lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.ktlsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.ktlsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.ktlsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.ktlsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt
…iedForPath
psiManager.findFile / structureProvider.getModule / findModuleForSourceId ran
without holding the read lock, so they could race a concurrent `analyze` read
section. Resolve them inside project.read { } (mirroring loadKtFile) and hand
the results to the existing project.write { } mutation block.
See ADFA-4387 for more details.