From 5b9ff7a3c20a2ec560f66b8dbee453ea9a47526d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Raddum=20Berg?= Date: Fri, 31 Jul 2026 22:58:35 +0200 Subject: [PATCH] diagnostics: say what stop-all deletes, flag missing server logs, unbreak bleep fmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fallout from an OOM incident report against M10, where the evidence trail went cold twice: - `bleep config compile-server stop-all` deleted a socket dir holding 10GB of heap dumps without a word. It still deletes — that is its job — but now logs each dir with its size, at warn level when it was ≥1GB, so gigabytes of diagnostics no longer vanish silently. - The failure path printed `BSP server log: ` for a file that did not exist (server never started, or the dir was already cleaned), sending the reader to a dead end. Both call sites now say `(missing — …)` when the file is not there. Separately, `bleep fmt` failed repo-wide: scalafmt cannot parse a lambda whose body is an indented try/catch followed by `, arg` in the same call (ServerRunInterruptTest.scala, from #629). Verified against scalafmt 3.11.5 under dialects scala3/scala36/scala3future — none parse it, so the fix is binding the Runnable to a val first. Bumped scalafmt 3.11.1 → 3.11.5 while at it; zero reformat churn. Co-Authored-By: Claude Fable 5 --- .scalafmt.conf | 2 +- .../scala/bleep/bsp/ServerRunInterruptTest.scala | 11 ++++++----- .../scala/bleep/commands/CompileServerStopAll.scala | 13 +++++++++++++ .../src/scala/bleep/commands/ReactiveBsp.scala | 13 +++++++++++-- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 2d439d3ec..402c8da97 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version=3.11.1 +version=3.11.5 maxColumn = 160 rewrite.rules = [SortImports, RedundantBraces, RedundantParens, PreferCurlyFors] project.excludePaths = [ diff --git a/bleep-bsp-tests/src/scala/bleep/bsp/ServerRunInterruptTest.scala b/bleep-bsp-tests/src/scala/bleep/bsp/ServerRunInterruptTest.scala index fb9a6ce1c..2369f4ec6 100644 --- a/bleep-bsp-tests/src/scala/bleep/bsp/ServerRunInterruptTest.scala +++ b/bleep-bsp-tests/src/scala/bleep/bsp/ServerRunInterruptTest.scala @@ -22,11 +22,12 @@ class ServerRunInterruptTest extends AnyFunSuite with Matchers { val parked = new CountDownLatch(1) val thrown = new AtomicReference[Option[Throwable]](None) - val t = new Thread( - () => - try body(parked) - catch { case e: Throwable => thrown.set(Some(e)) }, "interrupt-under-test" - ) + // scalafmt's parser (any version/dialect as of 3.11.5) cannot parse a lambda whose body is an + // indented try/catch followed by `, arg` in the same call — bind the Runnable first. + val runnable: Runnable = () => + try body(parked) + catch { case e: Throwable => thrown.set(Some(e)) } + val t = new Thread(runnable, "interrupt-under-test") t.setDaemon(true) t.start() diff --git a/bleep-cli/src/scala/bleep/commands/CompileServerStopAll.scala b/bleep-cli/src/scala/bleep/commands/CompileServerStopAll.scala index 7f345b4aa..75d835be1 100644 --- a/bleep-cli/src/scala/bleep/commands/CompileServerStopAll.scala +++ b/bleep-cli/src/scala/bleep/commands/CompileServerStopAll.scala @@ -7,6 +7,7 @@ import cats.effect.unsafe.implicits.global import ryddig.Logger import java.nio.file.{Files, Path} +import scala.jdk.CollectionConverters.IteratorHasAsScala import scala.jdk.StreamConverters.StreamHasToScala case class CompileServerStopAll(logger: Logger, userPaths: UserPaths) extends BleepCommand { @@ -29,6 +30,7 @@ case class CompileServerStopAll(logger: Logger, userPaths: UserPaths) extends Bl // Give the OS time to release file handles after process kill Thread.sleep(200) + val sizeMb = dirSizeBytes(socketDir) / (1024L * 1024L) try FileUtils.deleteDirectory(socketDir) catch { case _: java.nio.file.DirectoryNotEmptyException | _: java.nio.file.FileSystemException => @@ -37,7 +39,18 @@ case class CompileServerStopAll(logger: Logger, userPaths: UserPaths) extends Bl Thread.sleep(2000) FileUtils.deleteDirectory(socketDir) } + // These dirs can hold multi-GB diagnostic artifacts (heap dumps from pre-M11 servers, output + // logs). Deleting gigabytes without a word once cost a user the evidence for an OOM report — + // say what went away, and stand out when it was big. + val msg = s"deleted $socketDir (${sizeMb}MB)" + if (sizeMb >= 1024) logger.warn(msg) else logger.info(msg) } Right(()) } + + private def dirSizeBytes(dir: Path): Long = { + val stream = Files.walk(dir) + try stream.iterator().asScala.filter(Files.isRegularFile(_)).map(Files.size).sum + finally stream.close() + } } diff --git a/bleep-core/src/scala/bleep/commands/ReactiveBsp.scala b/bleep-core/src/scala/bleep/commands/ReactiveBsp.scala index 3e571c52d..ec723ca54 100644 --- a/bleep-core/src/scala/bleep/commands/ReactiveBsp.scala +++ b/bleep-core/src/scala/bleep/commands/ReactiveBsp.scala @@ -457,7 +457,7 @@ case class ReactiveBsp( _ <- display.printSummary(filterContext) // Update previousRunState from collected events (only in DiffWatch mode) _ <- if (isDiffWatch) IO(previousRunState.set(PreviousRunState.fromEvents(collectedBuildEvents.get().reverse))) else IO.unit - _ <- IO.delay(started.logger.info(s" BSP server log: ${BspRifle.getOutputFile(config)}")) + _ <- IO.delay(started.logger.info(serverLogLine(config))) _ <- if (flamegraph) IO.delay(started.logger.info(s" Flamegraph: ${started.buildPaths.dotBleepDir.resolve("trace.json")} (open in chrome://tracing or ui.perfetto.dev)")) @@ -498,12 +498,21 @@ case class ReactiveBsp( case None => printCancelledSummary(started, durationMs) } } - started.logger.info(s" BSP server log: ${BspRifle.getOutputFile(config)}") + started.logger.info(serverLogLine(config)) if (flamegraph) started.logger.info(s" Flamegraph: ${started.buildPaths.dotBleepDir.resolve("trace.json")} (open in chrome://tracing or ui.perfetto.dev)") } } + /** The failure path can point at a log that is not there — the server never started, or the socket dir was deleted (`compile-server stop-all`, per-invocation + * shutdown hook). Saying so beats sending the reader to a file that does not exist. + */ + private def serverLogLine(config: BspRifleConfig): String = { + val outputFile = BspRifle.getOutputFile(config) + if (java.nio.file.Files.exists(outputFile)) s" BSP server log: $outputFile" + else s" BSP server log: $outputFile (missing — the server never started, or its directory was deleted)" + } + private def printErrorSummary(started: Started, err: Throwable, durationMs: Long): Unit = { import bleep.testing.BleepConsole as C val logger = started.logger