From b2fea15030706aa30c7ec87790659479660e878a Mon Sep 17 00:00:00 2001 From: PandaMan Date: Mon, 9 Feb 2026 02:28:54 -0500 Subject: [PATCH] [2.x] fix: Handle CancellationException gracefully with usePipelining (#8718) When usePipelining is enabled and compilation has errors, CancellationException was being thrown and showing confusing stack traces to users. This fix catches the exception in ConcurrentRestrictions.take() and converts it to Incomplete, which is properly handled by the task execution framework without showing stack traces. - Added CancellationException import - Wrapped jservice.take().get() in try-catch - Convert CancellationException to Incomplete to prevent stack traces - Added scripted test to verify the fix Fixes #7973 --- .../project/usePipelining-cancellation/build.sbt | 8 ++++++++ .../src/main/scala/A.scala | 7 +++++++ .../project/usePipelining-cancellation/test | 7 +++++++ .../main/scala/sbt/ConcurrentRestrictions.scala | 14 ++++++++++++-- 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 sbt-app/src/sbt-test/project/usePipelining-cancellation/build.sbt create mode 100644 sbt-app/src/sbt-test/project/usePipelining-cancellation/src/main/scala/A.scala create mode 100644 sbt-app/src/sbt-test/project/usePipelining-cancellation/test diff --git a/sbt-app/src/sbt-test/project/usePipelining-cancellation/build.sbt b/sbt-app/src/sbt-test/project/usePipelining-cancellation/build.sbt new file mode 100644 index 000000000..69344aa97 --- /dev/null +++ b/sbt-app/src/sbt-test/project/usePipelining-cancellation/build.sbt @@ -0,0 +1,8 @@ +// Test for issue #7973: CancellationException should not be thrown on compile errors with usePipelining +ThisBuild / usePipelining := true + +lazy val root = (project in file(".")) + .settings( + name := "usePipelining-cancellation", + scalaVersion := "2.13.15" + ) diff --git a/sbt-app/src/sbt-test/project/usePipelining-cancellation/src/main/scala/A.scala b/sbt-app/src/sbt-test/project/usePipelining-cancellation/src/main/scala/A.scala new file mode 100644 index 000000000..465e4255e --- /dev/null +++ b/sbt-app/src/sbt-test/project/usePipelining-cancellation/src/main/scala/A.scala @@ -0,0 +1,7 @@ +object A { + def invalidSyntax { + // Missing closing brace - this will cause compile errors + val x = 1 + // } +} + diff --git a/sbt-app/src/sbt-test/project/usePipelining-cancellation/test b/sbt-app/src/sbt-test/project/usePipelining-cancellation/test new file mode 100644 index 000000000..5394963f4 --- /dev/null +++ b/sbt-app/src/sbt-test/project/usePipelining-cancellation/test @@ -0,0 +1,7 @@ +# Test for issue #7973: CancellationException should not be thrown on compile errors with usePipelining +# This verifies that compile errors with usePipelining enabled don't show CancellationException stack traces + +# Try to compile (should fail with compile errors, but not throw CancellationException) +# The fix ensures CancellationException is caught and handled gracefully +# Using -> to indicate expected failure +-> compile diff --git a/tasks/src/main/scala/sbt/ConcurrentRestrictions.scala b/tasks/src/main/scala/sbt/ConcurrentRestrictions.scala index 1f35f8e7a..bb5ae2257 100644 --- a/tasks/src/main/scala/sbt/ConcurrentRestrictions.scala +++ b/tasks/src/main/scala/sbt/ConcurrentRestrictions.scala @@ -12,7 +12,7 @@ import java.util.concurrent.atomic.AtomicInteger import sbt.internal.util.AttributeKey import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.{ Future as JFuture, RejectedExecutionException } +import java.util.concurrent.{ Future as JFuture, RejectedExecutionException, CancellationException } import scala.collection.mutable import scala.jdk.CollectionConverters.* @@ -312,7 +312,17 @@ object ConcurrentRestrictions { throw new RejectedExecutionException( "Tried to get values for a closed completion service" ) - jservice.take().get() + try { + jservice.take().get() + } catch { + case ce: CancellationException => + // When tasks are cancelled (e.g., due to compile errors with usePipelining), + // the future's get() throws CancellationException. Convert this to an Incomplete + // to avoid showing stack traces to users. The cancellation is already handled + // by the task execution framework, so we just need to prevent the exception + // from propagating as an unhandled exception. + throw Incomplete(node = None, message = Some("cancelled"), directCause = Some(ce)) + } } } }