UI freeze: deadlock between main thread and LSP listener on quota fallback · Issue #179 · microsoft/copilot-for-eclipse · GitHub
Skip to content

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

Closed

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 error → ChatContentViewer (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+)

Activity

  1. jdneo commented on May 12, 2026

    @jdneo
    MemberAuthor

    There is another bug, when quota exceeded, it will render a lot of warning dialog:

    Image
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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