Skip to content

ADFA-4174: prioritize code completion requests over over analyze requests#1439

Open
itsaky-adfa wants to merge 11 commits into
stagefrom
fix/ADFA-4174
Open

ADFA-4174: prioritize code completion requests over over analyze requests#1439
itsaky-adfa wants to merge 11 commits into
stagefrom
fix/ADFA-4174

Conversation

@itsaky-adfa

@itsaky-adfa itsaky-adfa commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

See ADFA-4174 for more details.

itsaky and others added 4 commits June 19, 2026 22:28
Signed-off-by: Akash Yadav <itsaky01@gmail.com>
Signed-off-by: Akash Yadav <itsaky01@gmail.com>
… 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).
Replace the FIFO analysis lock with AnalysisScheduler, a process-global,
priority-aware, preemptive, reentrant lock that serializes all Kotlin Analysis
API access while letting interactive work win.

Priority order: Completion > Diagnostics > Indexing.
- A higher-priority request preempts a strictly lower-priority in-progress
  analysis. Preemption is cooperative (the Analysis API can't be interrupted
  mid-analyze): the holder's ScheduledCancelChecker is flagged and the running
  analysis bails at its next abortIfCancelled() checkpoint with
  AnalysisPreemptedException.
- A lower-priority request waits while a higher-priority one holds.
- Preempted diagnostics/indexing work is auto-rescheduled so it still completes:
  diagnostics re-schedules via the fileAnalyzer; indexing re-queues the command.
- Indexing is now actively preemptible (was ICancelChecker.NOOP).

Wiring:
- KotlinCompletions: COMPLETION priority, request-scoped checker from Lookup.
- KotlinDiagnosticProvider: DIAGNOSTICS priority; CompilationEnvironment's
  fileAnalyzer catches AnalysisPreemptedException to re-schedule.
- IndexWorker/SourceFileIndexer: INDEXING priority; re-queue on preemption.

Tests: extend AnalysisSerializationTest with reentrancy, higher-preempts-lower,
and lower-waits-for-higher cases (all 5 tests pass).
@itsaky-adfa itsaky-adfa requested a review from a team June 24, 2026 15:06
@itsaky-adfa itsaky-adfa self-assigned this Jun 24, 2026
Signed-off-by: Akash Yadav <akashyadav@appdevforall.org>
…checker

ICancelChecker.invokeOnCancel(listener) pushes on cancel (fire-once, immediate if already cancelled, no-op on NOOP). ProgressManager gains register/unregister so cancel(thread) flips the request's own checker. Adds ICancelCheckerTest and ProgressManagerTest.
withAnalysisLock installs a cancellable Job and registers an invokeOnCancel listener so both scheduler preemption and ordinary editor cancellation abort mid-analyze via a single push (no polling). AnalysisScheduler.acquire is cancellation-aware while waiting so superseded requests bail instead of parking. KotlinCompletions uses params.cancelChecker (race-free) and classifies all cancellation exception types uniformly. Adds AnalysisSerializationTest coverage.
EditorCompletionWindow debounces keystrokes so at most one completion analysis runs at a time for the latest cursor position, preventing the CompletionThread/allocation pile-up. IDELanguage registers the request's CompletionCancelChecker on its thread so the editor's cancel routes to it and pushes the mid-analyze abort.
@itsaky-adfa itsaky-adfa marked this pull request as ready for review July 4, 2026 03:40

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@itsaky-adfa

Copy link
Copy Markdown
Contributor Author

@claude review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, comment @claude review on this pull request to trigger a review.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough
  • Prioritized Kotlin code completion ahead of diagnostics/indexing by introducing a global, priority-aware analysis scheduler with cooperative preemption and reentrant locking.

  • Added cancellation integration across analysis/completion paths, including push-based cancel listeners, thread registration with ProgressManager, and prompt abort behavior during compiler checkpoints.

  • Refactored completion scheduling to debounce rapid requests, cancel pending/in-flight work, and coalesce to the latest completion request.

  • Updated diagnostics and indexing workers to detect preemption, reschedule interrupted work, and use cancellable checkers during analysis.

  • Added helper infrastructure for thread-context/job installation and progress-indicator bridging so Kotlin analysis can be cancelled more reliably.

  • Expanded regression coverage for scheduler behavior, cancellation semantics, completion supersession, and progress-manager registration/unregistration.

  • Risks / best-practice notes:

    • The new global scheduler increases concurrency complexity and introduces preemption races that rely on correct cancellation checkpoints.
    • Completion, diagnostics, and indexing now depend on coordinated cancellation/rescheduling behavior; missed checkpoints could leave work partially executed or delayed.
    • The added --add-opens JVM argument is a test-environment workaround and should be kept narrowly scoped.

Walkthrough

This PR introduces a process-global, priority-aware, preemptive AnalysisScheduler for Kotlin Analysis API calls, replacing a simple lock. It adds push-based cancellation (invokeOnCancel) to ICancelChecker, thread-registered cancellation in ProgressManager, and propagates priority/preemption handling through indexing, diagnostics, and completion pipelines. Editor completion gains debounced scheduling.

Changes

Analysis scheduling and cancellation infrastructure

Layer / File(s) Summary
Push-based cancellation and thread registration
shared/src/main/java/com/itsaky/androidide/progress/ICancelChecker.kt, ProgressManager.kt, shared/src/test/java/com/itsaky/androidide/progress/*
ICancelChecker gains invokeOnCancel; ProgressManager adds register/unregister per-thread checkers with synchronized cancel/abortIfCancelled; new tests validate both.
Priority-aware preemptive scheduler
lsp/kotlin/.../compiler/modules/AnalysisScheduler.kt, AnalysisThreadContext.java, CancelCheckerProgressIndicator.kt
Adds AnalysisPriority, AnalysisPreemptedException, ScheduledCancelChecker, a reentrant priority-aware lock (AnalysisScheduler), coroutine Job context installation, and a progress indicator bridging cancellation checks.
withAnalysisLock/analyzeMaybeDangling rework
lsp/kotlin/.../compiler/modules/KtFileExts.kt
withAnalysisLock and analyzeMaybeDangling now take priority and ScheduledCancelChecker, route through AnalysisScheduler, and convert ProcessCanceledException via cancellation re-checks.
Indexing preemption handling
lsp/kotlin/.../compiler/index/SourceFileIndexer.kt, IndexWorker.kt
Indexing wraps checkers, adds abort checkpoints, passes AnalysisPriority.INDEXING, and re-queues preempted indexing commands.
Diagnostics preemption handling
lsp/kotlin/.../compiler/CompilationEnvironment.kt, diagnostic/KotlinDiagnosticProvider.kt
Diagnostics collection catches preemption to reschedule, and uses ScheduledCancelChecker with AnalysisPriority.DIAGNOSTICS.
Completion cancellation and debounce
lsp/kotlin/.../completion/KotlinCompletions.kt, editor/.../IDELanguage.kt, editor/.../EditorCompletionWindow.kt
Completion classifies cancellation uniformly, bounds unimported symbol fetch, registers with ProgressManager, and debounces completion requests via pendingCompletion.
Scheduler regression tests
lsp/kotlin/src/test/.../AnalysisSerializationTest.kt
Updates existing tests to the new overload and adds tests for reentrancy, preemption, cancellation, and same-priority supersession.
Build/repo config
build.gradle.kts, .claude/.gitignore
Adds --add-opens for java.util.concurrent in test JVM args and ignores Claude Code local artifacts.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EditorCompletionWindow
  participant ProgressManager
  participant AnalysisScheduler
  participant KotlinCompletions

  EditorCompletionWindow->>EditorCompletionWindow: requireCompletion() debounce
  EditorCompletionWindow->>ProgressManager: register(completionThread, checker)
  EditorCompletionWindow->>KotlinCompletions: doComplete(params)
  KotlinCompletions->>AnalysisScheduler: acquire(COMPLETION, checker)
  AnalysisScheduler-->>KotlinCompletions: preempt lower-priority holder if needed
  KotlinCompletions-->>EditorCompletionWindow: CompletionResult / AnalysisPreemptedException
Loading
sequenceDiagram
  participant IndexWorker
  participant SourceFileIndexer
  participant AnalysisScheduler

  IndexWorker->>SourceFileIndexer: indexSourceFile(checker)
  SourceFileIndexer->>AnalysisScheduler: analyzeMaybeDangling(INDEXING, checker)
  AnalysisScheduler-->>SourceFileIndexer: AnalysisPreemptedException
  SourceFileIndexer-->>IndexWorker: propagate exception
  IndexWorker->>IndexWorker: re-queue IndexCommand via submitCommand
Loading

Possibly related PRs

Suggested reviewers: dara-abijo-adfa, Daniel-ADFA

Poem

A rabbit hops through locks and queues,
Preempting whoever it may choose. 🐇
Cancel checkers ring their bell,
Diagnostics reschedule swell,
Debounced keystrokes, no more abuse!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title names the main feature area and ADFA-4174, clearly conveying completion priority over analysis despite a typo.
Description check ✅ Passed The description points to the linked ADFA-4174 ticket, which is related to the implemented changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/ADFA-4174

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt (1)

160-162: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: silence detekt SwallowedException on the intentional preemption catches.

Catching AnalysisPreemptedException only to flip a flag is intentional here, but detekt flags the swallowed exception at Lines 160, 205, and 384. If detekt runs as a build gate, either reference the exception (e.g. rename to _) or add a brief justifying comment to keep it quiet without behavior change.

♻️ Example
-			} catch (e: AnalysisPreemptedException) {
+			} catch (_: AnalysisPreemptedException) {
 				preempted.set(true)
 			}

Also applies to: 205-207, 384-386

🤖 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 160 - 162, The intentional AnalysisPreemptedException catches in
AnalysisSerializationTest are being flagged by detekt as swallowed exceptions;
update the affected catch blocks in the test methods to make the intent
explicit, either by referencing the exception variable (for example with an
underscore-style naming convention if supported) or by adding a brief
explanatory comment right where the catch occurs. Keep the behavior the same in
the relevant catch sites around the AnalysisPreemptedException handling in
AnalysisSerializationTest, especially the preemption flag logic.

Source: Linters/SAST tools

🤖 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/modules/KtFileExts.kt`:
- Around line 76-107: The cancellation hook setup around
AnalysisScheduler.acquire and AnalysisScheduler.release is outside the
lock-guarded try/finally, so a failure in cancelChecker.invokeOnCancel can
bypass release and deadlock Kotlin analysis globally. Move the invokeOnCancel
registration into the same try block that already wraps
AnalysisThreadContext.installJob and
ProgressManager.executeProcessUnderProgress, keeping AnalysisScheduler.release
in the finally so the lock is always released. Use the existing symbols
AnalysisScheduler.acquire, cancelChecker.invokeOnCancel, and
AnalysisScheduler.release to place the fix correctly.

In `@shared/src/main/java/com/itsaky/androidide/progress/ProgressManager.kt`:
- Around line 73-81: The race in `ProgressManager.abortIfCancelled()` comes from
checking `threads[thisThread]` and removing it in separate synchronized
sections, which can delete a newer registration for the same thread. Update
`abortIfCancelled()` so the lookup, cancellation check, and conditional
`threads.remove(thisThread)` happen atomically within one
`synchronized(threads)` block, using the current `checker` only if it is still
the same entry you observed.

---

Nitpick comments:
In
`@lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt`:
- Around line 160-162: The intentional AnalysisPreemptedException catches in
AnalysisSerializationTest are being flagged by detekt as swallowed exceptions;
update the affected catch blocks in the test methods to make the intent
explicit, either by referencing the exception variable (for example with an
underscore-style naming convention if supported) or by adding a brief
explanatory comment right where the catch occurs. Keep the behavior the same in
the relevant catch sites around the AnalysisPreemptedException handling in
AnalysisSerializationTest, especially the preemption flag logic.
🪄 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: 8af68e26-ff1a-4dd5-ae48-25d3b719ee53

📥 Commits

Reviewing files that changed from the base of the PR and between bf89f5b and e2f579b.

📒 Files selected for processing (18)
  • .claude/.gitignore
  • build.gradle.kts
  • editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt
  • editor/src/main/java/com/itsaky/androidide/editor/ui/EditorCompletionWindow.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexer.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisScheduler.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisThreadContext.java
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/CancelCheckerProgressIndicator.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt
  • lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt
  • lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisSerializationTest.kt
  • shared/src/main/java/com/itsaky/androidide/progress/ICancelChecker.kt
  • shared/src/main/java/com/itsaky/androidide/progress/ProgressManager.kt
  • shared/src/test/java/com/itsaky/androidide/progress/ICancelCheckerTest.kt
  • shared/src/test/java/com/itsaky/androidide/progress/ProgressManagerTest.kt

Comment on lines +76 to +107
AnalysisScheduler.acquire(priority, cancelChecker) {
// Preemption is signalled by flipping the checker; the invokeOnCancel listener below turns that
// (and ordinary editor cancellation) into the actual mid-`analyze` abort.
cancelChecker.preempt()
}
// Single push path for *both* preemption and ordinary editor cancellation: [ScheduledCancelChecker]
// fires this on preempt() and on its delegate's cancel(), so the running analyze aborts immediately
// — no polling, and unaffected by GC pauses that would stall a poll thread.
cancelChecker.invokeOnCancel {
indicator.cancel()
job.cancel()
}
try {
val holder = arrayOfNulls<Any?>(1)
try {
// Install the Job for the duration of the analysis and restore the previous context after.
AnalysisThreadContext.installJob(job).use {
ProgressManager.getInstance()
.executeProcessUnderProgress({ holder[0] = action() }, indicator)
}
} catch (e: ProcessCanceledException) {
logger.debug("process cancelled: prio={}", priority)
// Re-derive the semantically-correct exception callers expect (re-throws
// AnalysisPreemptedException when preempted, or the delegate's CancellationException).
cancelChecker.abortIfCancelled()
throw e
}
@Suppress("UNCHECKED_CAST")
return holder[0] as R
} finally {
AnalysisScheduler.release()
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move cancelChecker.invokeOnCancel { ... } inside the try/finally that releases the lock.

AnalysisScheduler.acquire(...) (Line 76-80) and cancelChecker.invokeOnCancel { ... } (Line 84-87) both run before the try { ... } finally { AnalysisScheduler.release() } block starts. If invokeOnCancel throws for any reason after the lock has already been granted, AnalysisScheduler.release() is never called — and since AnalysisScheduler is a process-global singleton lock, this permanently deadlocks all subsequent Kotlin analysis (completion, diagnostics, indexing) until the process restarts.

The current ScheduledCancelChecker.invokeOnCancel/Default.invokeOnCancel implementations don't appear to throw in practice, so the risk is currently low, but the blast radius (total analysis lockup) is severe enough to warrant defensive placement inside the lock-guarded region.

🔒 Proposed fix
 	AnalysisScheduler.acquire(priority, cancelChecker) {
 		// Preemption is signalled by flipping the checker; the invokeOnCancel listener below turns that
 		// (and ordinary editor cancellation) into the actual mid-`analyze` abort.
 		cancelChecker.preempt()
 	}
-	// Single push path for *both* preemption and ordinary editor cancellation: [ScheduledCancelChecker]
-	// fires this on preempt() and on its delegate's cancel(), so the running analyze aborts immediately
-	// — no polling, and unaffected by GC pauses that would stall a poll thread.
-	cancelChecker.invokeOnCancel {
-		indicator.cancel()
-		job.cancel()
-	}
 	try {
+		// Single push path for *both* preemption and ordinary editor cancellation: [ScheduledCancelChecker]
+		// fires this on preempt() and on its delegate's cancel(), so the running analyze aborts immediately
+		// — no polling, and unaffected by GC pauses that would stall a poll thread.
+		cancelChecker.invokeOnCancel {
+			indicator.cancel()
+			job.cancel()
+		}
 		val holder = arrayOfNulls<Any?>(1)
 		try {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AnalysisScheduler.acquire(priority, cancelChecker) {
// Preemption is signalled by flipping the checker; the invokeOnCancel listener below turns that
// (and ordinary editor cancellation) into the actual mid-`analyze` abort.
cancelChecker.preempt()
}
// Single push path for *both* preemption and ordinary editor cancellation: [ScheduledCancelChecker]
// fires this on preempt() and on its delegate's cancel(), so the running analyze aborts immediately
// — no polling, and unaffected by GC pauses that would stall a poll thread.
cancelChecker.invokeOnCancel {
indicator.cancel()
job.cancel()
}
try {
val holder = arrayOfNulls<Any?>(1)
try {
// Install the Job for the duration of the analysis and restore the previous context after.
AnalysisThreadContext.installJob(job).use {
ProgressManager.getInstance()
.executeProcessUnderProgress({ holder[0] = action() }, indicator)
}
} catch (e: ProcessCanceledException) {
logger.debug("process cancelled: prio={}", priority)
// Re-derive the semantically-correct exception callers expect (re-throws
// AnalysisPreemptedException when preempted, or the delegate's CancellationException).
cancelChecker.abortIfCancelled()
throw e
}
@Suppress("UNCHECKED_CAST")
return holder[0] as R
} finally {
AnalysisScheduler.release()
}
AnalysisScheduler.acquire(priority, cancelChecker) {
// Preemption is signalled by flipping the checker; the invokeOnCancel listener below turns that
// (and ordinary editor cancellation) into the actual mid-`analyze` abort.
cancelChecker.preempt()
}
try {
// Single push path for *both* preemption and ordinary editor cancellation: [ScheduledCancelChecker]
// fires this on preempt() and on its delegate's cancel(), so the running analyze aborts immediately
// — no polling, and unaffected by GC pauses that would stall a poll thread.
cancelChecker.invokeOnCancel {
indicator.cancel()
job.cancel()
}
val holder = arrayOfNulls<Any?>(1)
try {
// Install the Job for the duration of the analysis and restore the previous context after.
AnalysisThreadContext.installJob(job).use {
ProgressManager.getInstance()
.executeProcessUnderProgress({ holder[0] = action() }, indicator)
}
} catch (e: ProcessCanceledException) {
logger.debug("process cancelled: prio={}", priority)
// Re-derive the semantically-correct exception callers expect (re-throws
// AnalysisPreemptedException when preempted, or the delegate's CancellationException).
cancelChecker.abortIfCancelled()
throw e
}
`@Suppress`("UNCHECKED_CAST")
return holder[0] as R
} finally {
AnalysisScheduler.release()
}
🤖 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/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt`
around lines 76 - 107, The cancellation hook setup around
AnalysisScheduler.acquire and AnalysisScheduler.release is outside the
lock-guarded try/finally, so a failure in cancelChecker.invokeOnCancel can
bypass release and deadlock Kotlin analysis globally. Move the invokeOnCancel
registration into the same try block that already wraps
AnalysisThreadContext.installJob and
ProgressManager.executeProcessUnderProgress, keeping AnalysisScheduler.release
in the finally so the lock is always released. Use the existing symbols
AnalysisScheduler.acquire, cancelChecker.invokeOnCancel, and
AnalysisScheduler.release to place the fix correctly.

Comment on lines 73 to 81
@JvmName("internalAbortIfCancelled")
private fun abortIfCancelled() {
val thisThread = Thread.currentThread()
val checker = threads[thisThread]
val checker = synchronized(threads) { threads[thisThread] }
if (checker != null && checker.isCancelled()) {
threads.remove(thisThread)
synchronized(threads) { threads.remove(thisThread) }
throw CancellationException()
}
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

TOCTOU race in abortIfCancelled().

The read of threads[thisThread] and the removal happen in two separate synchronized(threads) blocks. Between them, another thread could register() a new checker for thisThread (e.g. a subsequent completion request reusing the same worker thread), and this method would then erroneously remove that new, unrelated registration instead of the stale one it actually observed as cancelled.

Combine the check and the removal into a single synchronized block to make the whole operation atomic.

🔒 Proposed fix
 	`@JvmName`("internalAbortIfCancelled")
 	private fun abortIfCancelled() {
 		val thisThread = Thread.currentThread()
-		val checker = synchronized(threads) { threads[thisThread] }
-		if (checker != null && checker.isCancelled()) {
-			synchronized(threads) { threads.remove(thisThread) }
-			throw CancellationException()
+		synchronized(threads) {
+			val checker = threads[thisThread]
+			if (checker != null && checker.isCancelled()) {
+				threads.remove(thisThread)
+				throw CancellationException()
+			}
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@JvmName("internalAbortIfCancelled")
private fun abortIfCancelled() {
val thisThread = Thread.currentThread()
val checker = threads[thisThread]
val checker = synchronized(threads) { threads[thisThread] }
if (checker != null && checker.isCancelled()) {
threads.remove(thisThread)
synchronized(threads) { threads.remove(thisThread) }
throw CancellationException()
}
}
`@JvmName`("internalAbortIfCancelled")
private fun abortIfCancelled() {
val thisThread = Thread.currentThread()
synchronized(threads) {
val checker = threads[thisThread]
if (checker != null && checker.isCancelled()) {
threads.remove(thisThread)
throw CancellationException()
}
}
}
🤖 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 `@shared/src/main/java/com/itsaky/androidide/progress/ProgressManager.kt`
around lines 73 - 81, The race in `ProgressManager.abortIfCancelled()` comes
from checking `threads[thisThread]` and removing it in separate synchronized
sections, which can delete a newer registration for the same thread. Update
`abortIfCancelled()` so the lookup, cancellation check, and conditional
`threads.remove(thisThread)` happen atomically within one
`synchronized(threads)` block, using the current `checker` only if it is still
the same entry you observed.

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