Skip to content

UI freeze: deadlock between main thread and LSP listener on quota fallback #179

@jdneo

Description

@jdneo

Bug Description

The Eclipse UI freezes permanently when a 402 quota error triggers the fallback model flow. A deadlock occurs between the main (UI) thread and the LSP message listener thread.

Deadlock Chain

Thread 1: "main" (UI thread) — WAITING

CompletableFuture.get()                          ← blocks UI thread indefinitely
  ChatBaseService.getPersistentFilePath(:125)     ← lsConnection.persistence().get()
  ChatBaseService.persistUserPreference(:86)      ← synchronized method
  ModelService.setActiveModel(:351)
  ModelService.setFallBackModelAsActiveModel(:390)
  ChatContentViewer.lambda$2(:240)                ← SWT async runnable on UI thread
  SWT Display.readAndDispatch event loop

Thread 2: "LS-...#listener-0" (LSP listener) — WAITING

Display.syncExec()                               ← blocks waiting for UI thread
  SwtUtils.invokeOnDisplayThread(:81)
  ChatContentViewer.processTurnEvent(:171)
  ChatView.onChatProgress(:881)
  CopilotLanguageClient.notifyProgress(:374)
  LSP4J StreamMessageProducer.listen              ← single-threaded message reader

What Happens

  1. User hits a 402 quota errorChatContentViewer (line 238–240) calls setFallBackModelAsActiveModel() on the UI thread
  2. That calls persistUserPreference()getPersistentFilePath()CompletableFuture.get() — blocking the UI thread waiting for the LSP server to respond
  3. Meanwhile, the Copilot language server sends a chat progress notification
  4. The LSP listener thread tries to deliver it via Display.syncExec() — which needs the UI thread to dispatch
  5. The UI thread is blocked waiting on the LSP response, and the LSP listener is the only thread reading server messages — so the persistence response can never arrive

Result: Classic circular wait → permanent UI freeze.

Root Cause

ChatBaseService.getPersistentFilePath() (line 125) calls this.lsConnection.persistence().get() — an unbounded blocking call — inside a synchronized method that executes on the UI thread. This violates the Eclipse threading rule: never block the UI thread on async results that depend on the same event loop.

Suggested Fixes

  1. Primary: Don't call .get() on the UI thread in getPersistentFilePath(). Cache the persistence path eagerly at startup or use .thenAccept() to persist asynchronously.
  2. Secondary: Change processTurnEvent() from syncExec to asyncExec where possible, so the LSP listener thread isn't blocked by the UI thread.
  3. Safety net: At minimum, add a timeout — .get(5, TimeUnit.SECONDS) — to prevent a permanent freeze.

Reproduction

Trigger a 402 quota exceeded error while a chat response is actively streaming (so the LSP listener thread is busy delivering progress notifications via syncExec).

Environment

  • macOS (aarch64)
  • Java 21.0.1+12-LTS
  • Eclipse 2025-12 (4.37+)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions