Skip to content

improvement: Recover from an unresponsive Bloop server - #8529

Open
jozanek wants to merge 2 commits into
scalameta:mainfrom
jozanek:improvement/bloop-recover-wedged-server
Open

improvement: Recover from an unresponsive Bloop server#8529
jozanek wants to merge 2 commits into
scalameta:mainfrom
jozanek:improvement/bloop-recover-wedged-server

Conversation

@jozanek

@jozanek jozanek commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Problem

Metals sometimes can't connect to Bloop when a stale, unresponsive server is already running (#3146): BloopRifle.check only confirms the socket is connectable, not that the daemon is responsive, so a wedged server looks "running" and the connection fails with a generic "Failed to connect" error. The old fix (shelling out to bloopgun via coursier) is obsolete — Metals now uses bloop-rifle's in-process BloopRifle.exit().

What this does

When connecting to a pre-existing Bloop server fails, Metals stops the wedged server and cold-starts a fresh one (once), on both the initial connection and later reconnects. If it can't be stopped, the user gets actionable guidance ("run build-restart or stop the process manually") instead of a generic error.

Notes for reviewers

  • Recovery is wired at the setupServer level in BuildServerConnection.fromSockets so it covers both the initial connect and every reconnect (setupConnection); this replaces the previous recursive fromSockets retry.
  • ConnectionProvider is shared across all build servers — the new AlreadyReportedConnectException only suppresses the redundant generic "Failed to connect" popup when the user has already been shown a specific message; every other server/failure is unchanged.
  • BloopRifle.exit (ng-stop) runs on a dedicated daemon thread on purpose: it's a synchronous call over the possibly-stuck socket, so isolating it keeps a truly hung server from occupying a shared execution-context thread.

Closes #3146.

Summary by CodeRabbit

  • Bug Fixes
    • Improved recovery when the build server appears to be running but is unresponsive.
    • Added clearer error handling so users see a specific unresponsive-server message instead of duplicate generic connection errors.
    • Made connection startup more resilient by retrying a broader range of socket-open failures.
  • Tests
    • Added coverage for build-server recovery and retry behavior.

When Bloop reports itself as running but is wedged (it accepts the BSP
socket but never finishes `build/initialize`, or the socket disappears),
Metals could not connect and showed only a generic "Failed to connect"
error, leaving the user to run `build-restart` by hand. `BloopRifle.check`
only verifies the daemon socket is connectable, not that it is processing
requests, so a stuck server still looks "running".

Metals now recovers automatically on both the initial connection and later
reconnects: when connecting to a pre-existing server fails, it stops the
server via `BloopRifle.exit` and cold-starts a fresh one, retrying once.
The decision is a one-shot guard, so a server Metals just started is never
killed and recovery never thrashes. If the server cannot be stopped, the
user gets actionable guidance instead of a generic error.

`ng-stop` runs on an isolated daemon thread so a truly hung server cannot
occupy a shared execution-context thread.

Addresses scalameta#3146.
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5a6a7855-fc7c-4fa8-bdb1-1c8a2ae5aa7e

📥 Commits

Reviewing files that changed from the base of the PR and between 408a691 and f6157fe.

📒 Files selected for processing (1)
  • metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala
🚧 Files skipped from review as they are similar to previous changes (1)
  • metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala

📝 Walkthrough

Walkthrough

Adds one-shot recovery for a wedged Bloop build server: detects a reused server, stops it, polls until confirmed stopped, and shows a typed error message if it doesn't stop in time. Introduces AlreadyReportedConnectException, RecoverConnectAction retry policy, and recoverConnection wiring in BuildServerConnection and BloopServers, plus a new unit test suite and CI registration.

Changes

Wedged Bloop Server Recovery

Layer / File(s) Summary
Recovery contract types and error message
metals/.../BuildServerConnection.scala, metals/.../Messages.scala
Adds AlreadyReportedConnectException, the RecoverConnectAction sealed trait/decision logic (RecoverAndRetry/Retry/GiveUp), and a new UnresponsiveBloopServer LSP message.
setupServerWithRecovery and fromSockets wiring
metals/.../BuildServerConnection.scala
Extends fromSockets with a recoverConnection callback and replaces the prior retry flow with setupServerWithRecovery, which applies the RecoverConnectAction policy on non-fatal failures.
Wedged server detection and recoverFromWedgedServer
metals/.../BloopServers.scala
Adds recoverFromWedgedServer/awaitBloopStopped to stop and poll a reused server, refactors connect/adds startNewServer, wires the recovery callback into fromSockets, normalizes BSP socket failures, and adds recovery timing constants.
Suppress duplicate error in ConnectionProvider
metals/.../ConnectionProvider.scala
Skips the generic connection-failure message when the exception is AlreadyReportedConnectException.
Unit tests and CI registration
tests/unit/.../BuildServerConnectionRecoverySuite.scala, project/TestGroups.scala
Adds BuildServerConnectionRecoverySuite covering RecoverConnectAction outcomes and registers it in CI test groups.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BuildServerConnection
  participant recoverFromWedgedServer
  participant BloopRifle
  participant awaitBloopStopped
  participant languageClient

  BuildServerConnection->>recoverFromWedgedServer: recoverConnection() [connectedToPreexisting=true]
  recoverFromWedgedServer->>BloopRifle: exit() [daemon thread]
  recoverFromWedgedServer->>awaitBloopStopped: poll check() until deadline
  awaitBloopStopped->>BloopRifle: check() [scheduled retries]
  alt server stops in time
    BloopRifle-->>awaitBloopStopped: server down
    awaitBloopStopped-->>recoverFromWedgedServer: success
    recoverFromWedgedServer-->>BuildServerConnection: recovery complete
  else timeout
    recoverFromWedgedServer->>languageClient: showMessage(UnresponsiveBloopServer)
    recoverFromWedgedServer-->>BuildServerConnection: throw AlreadyReportedConnectException
  end
Loading
sequenceDiagram
  participant fromSockets
  participant setupServerWithRecovery
  participant setupServer
  participant recoverConnection

  fromSockets->>setupServerWithRecovery: invoke(retriesLeft, alreadyRecovered)
  setupServerWithRecovery->>setupServer: attempt connection
  setupServer-->>setupServerWithRecovery: NonFatal exception
  setupServerWithRecovery->>setupServerWithRecovery: RecoverConnectAction.apply(error, retriesLeft, alreadyRecovered)
  alt RecoverAndRetry
    setupServerWithRecovery->>recoverConnection: invoke()
    setupServerWithRecovery->>setupServerWithRecovery: recurse(retriesLeft-1, alreadyRecovered=true)
  else Retry
    setupServerWithRecovery->>setupServerWithRecovery: recurse(retriesLeft-1, alreadyRecovered)
  else GiveUp
    setupServerWithRecovery-->>fromSockets: Future.failed(error)
  end
Loading

Suggested reviewers: tgodzik

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: recovering from an unresponsive Bloop server.
Linked Issues check ✅ Passed The PR implements recovery for wedged Bloop connections and user guidance when recovery fails, matching #3146's core objective.
Out of Scope Changes check ✅ Passed The changes stay focused on Bloop connection recovery, related error messaging, and tests, with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala (1)

131-131: 💤 Low value

Consider extracting timeout constants for clarity.

The recovery timeout (10 seconds) and polling interval (100ms on line 162) are hardcoded. Extracting these as named constants in the companion object would improve readability and make them easier to tune if needed.

+object BloopServers {
+  val name = "Bloop"
+  private val RecoveryTimeoutMs = 10000L
+  private val RecoveryPollIntervalMs = 100L
+  // ...
+}

Then use RecoveryTimeoutMs and RecoveryPollIntervalMs in the implementation.

🤖 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 `@metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala` at line
131, Extract the hardcoded timeout values as named constants in the BloopServers
companion object. Create two constants: RecoveryTimeoutMs set to 10000 (for the
10-second timeout currently hardcoded in the awaitBloopStopped call) and
RecoveryPollIntervalMs set to 100 (for the polling interval). Then replace the
hardcoded value 10000 in the awaitBloopStopped call on line 131 with
RecoveryTimeoutMs, and replace the hardcoded polling interval value of 100 on
line 162 with RecoveryPollIntervalMs to improve code readability and
maintainability.
🤖 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 `@metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala`:
- Line 131: Extract the hardcoded timeout values as named constants in the
BloopServers companion object. Create two constants: RecoveryTimeoutMs set to
10000 (for the 10-second timeout currently hardcoded in the awaitBloopStopped
call) and RecoveryPollIntervalMs set to 100 (for the polling interval). Then
replace the hardcoded value 10000 in the awaitBloopStopped call on line 131 with
RecoveryTimeoutMs, and replace the hardcoded polling interval value of 100 on
line 162 with RecoveryPollIntervalMs to improve code readability and
maintainability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a00c1717-0aae-435d-bd73-e6dd77d94fd6

📥 Commits

Reviewing files that changed from the base of the PR and between 9c32f06 and 408a691.

📒 Files selected for processing (6)
  • metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala
  • metals/src/main/scala/scala/meta/internal/metals/BuildServerConnection.scala
  • metals/src/main/scala/scala/meta/internal/metals/ConnectionProvider.scala
  • metals/src/main/scala/scala/meta/internal/metals/Messages.scala
  • project/TestGroups.scala
  • tests/unit/src/test/scala/tests/BuildServerConnectionRecoverySuite.scala

@jozanek
jozanek marked this pull request as ready for review July 8, 2026 17:42
@jozanek

jozanek commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@tgodzik could we restart failing CI? It doesn't look like connected to my changes.

@tgodzik

tgodzik commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Looks like tests.sbt.SbtBloopLspSuite is failing for sbt 2, any idea if that is related? Doesn't seem to fail on main. I will rerun again to be sure.

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.

Restart Bloop via console when unable to connect via launcher

2 participants