diff --git a/main-settings/src/main/scala/sbt/PromiseWrap.scala b/main-settings/src/main/scala/sbt/PromiseWrap.scala index 8fa3d6aa3..ba485cfc7 100644 --- a/main-settings/src/main/scala/sbt/PromiseWrap.scala +++ b/main-settings/src/main/scala/sbt/PromiseWrap.scala @@ -17,6 +17,11 @@ final class PromiseWrap[A]: case Result.Inc(cause) => underlying.failure(cause) case Result.Value(value) => underlying.success(value) } + def tryComplete(result: Result[A]): Boolean = + result match { + case Result.Inc(cause) => underlying.tryFailure(cause) + case Result.Value(value) => underlying.trySuccess(value) + } def success(value: A): Unit = underlying.success(value) def failure(cause: Throwable): Unit = underlying.failure(cause) def isCompleted: Boolean = underlying.isCompleted diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index f5a080bb8..b3bed8b3f 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2259,8 +2259,13 @@ object Defaults extends BuildCommon with DefExtra { val ci = (compile / compileInputs).value val c = fileConverter.value val dir = c.toPath(backendOutput.value).toFile + val ping = (TaskZero / earlyOutputPing).value result match case Result.Value(res) => + // Zinc completes the ping during a real compile; on an action-cache hit + // zinc never runs, so complete it from the pickle jar. tryComplete is + // atomic: zinc's own completion always wins when it ran. + ping.tryComplete(Result.Value(c.toPath(earlyOutput.value).toFile.exists)) val af = compileAnalysisFile.value val store = analysisStore(af.toPath(), c) if !af.exists then sys.error(s"${af} is missing") @@ -2269,6 +2274,7 @@ object Defaults extends BuildCommon with DefExtra { bspTask.notifySuccess(analysis) res case Result.Inc(cause) => + ping.tryComplete(Result.Value(false)) val compileFailed = cause.directCause.collect { case c: CompileFailed => c } reporter.sendFailureReport(ci.options.sources, compileFailed) bspTask.notifyFailure(compileFailed) diff --git a/notes/2.0.0/pipelining-cache-hit-hang.md b/notes/2.0.0/pipelining-cache-hit-hang.md new file mode 100644 index 000000000..b4d9f6438 --- /dev/null +++ b/notes/2.0.0/pipelining-cache-hit-hang.md @@ -0,0 +1,3 @@ +### Fixes + +- Don't hang on `earlyOutputPing` when pipelining is enabled and a compile resolves from the action cache or fails. The ping was completed only as a side effect of zinc running, so a cache-hit or failed compile left downstream pipelined compiles waiting on it forever (`compile;compile` on a warm build hung indefinitely). The compile task now completes the ping on every resolution path: zinc's own completion still wins when zinc runs, a cache hit completes it from the pickle jar's presence, and a failure completes it false so waiters fall back to a full compile. Fixes [#9486](https://github.com/sbt/sbt/issues/9486). diff --git a/sbt-app/src/sbt-test/source-dependencies/pipelining/build.sbt b/sbt-app/src/sbt-test/source-dependencies/pipelining/build.sbt index 25dfae396..b37cf7cc0 100644 --- a/sbt-app/src/sbt-test/source-dependencies/pipelining/build.sbt +++ b/sbt-app/src/sbt-test/source-dependencies/pipelining/build.sbt @@ -12,11 +12,12 @@ lazy val dep = project lazy val use = project .dependsOn(dep) .settings( - TaskKey[Unit]("checkPickle") := { + TaskKey[Unit]("checkPickle") := Def.uncached { val s = streams.value val x = (dep / Compile / compile).value val picklePath = (Compile / internalDependencyPicklePath).value assert(picklePath.size == 1 && - picklePath.head.data.name == "early.jar", s"picklePath = ${picklePath}") + picklePath.head.data.name.endsWith(".jar") && + picklePath.head.data.toString.contains("early"), s"picklePath = ${picklePath}") }, ) diff --git a/sbt-app/src/sbt-test/source-dependencies/pipelining/disabled b/sbt-app/src/sbt-test/source-dependencies/pipelining/test similarity index 100% rename from sbt-app/src/sbt-test/source-dependencies/pipelining/disabled rename to sbt-app/src/sbt-test/source-dependencies/pipelining/test