Skip to content

Commit 9ae72fb

Browse files
authored
Merge pull request #51 from softwaremill/fix-self-suppression
Fix self-suppressed exceptions
2 parents d12ddbb + 383476a commit 9ae72fb

File tree

3 files changed

+44
-9
lines changed

3 files changed

+44
-9
lines changed

core/src/main/scala/ox/fork.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ def fork[T](f: => T)(using Ox): Fork[T] =
2525
supervisor.forkSuccess()
2626
catch
2727
case e: Throwable =>
28+
// we notify the supervisor first, so that if this is the first failing fork in the scope, the supervisor will
29+
// get first notified of the exception by the "original" (this) fork
30+
supervisor.forkError(e)
2831
result.completeExceptionally(e)
29-
supervisor.forkError(e)
3032
}
3133
newForkUsingResult(result)
3234

core/src/main/scala/ox/supervised.scala

+4-4
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ def supervised[T](f: Ox ?=> T): T =
2727
catch
2828
case e: Throwable =>
2929
// all forks are guaranteed to have finished: some might have ended up throwing exceptions (InterruptedException or
30-
// others), but only the first one is propagated. That's wait, adding the others as suppressed.
31-
s.addSuppressed(e)
30+
// others), but only the first one is propagated below. That's why we add all the other exceptions as suppressed.
31+
s.addOtherExceptionsAsSuppressedTo(e)
3232
throw e
3333

3434
trait Supervisor:
@@ -66,8 +66,8 @@ class DefaultSupervisor() extends Supervisor:
6666

6767
override def join(): Unit = unwrapExecutionException(result.get())
6868

69-
def addSuppressed(e: Throwable): Throwable =
70-
otherExceptions.forEach(e.addSuppressed)
69+
def addOtherExceptionsAsSuppressedTo(e: Throwable): Throwable =
70+
otherExceptions.forEach(e2 => if e != e2 then e.addSuppressed(e2))
7171
e
7272

7373
/** Change the supervisor that is being used when running `f`. Doesn't affect existing usages of the current supervisor, or forks ran

core/src/test/scala/ox/ExceptionTest.scala

+37-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import java.util.concurrent.Semaphore
99
class ExceptionTest extends AnyFlatSpec with Matchers {
1010
class CustomException extends RuntimeException
1111
class CustomException2 extends RuntimeException
12+
class CustomException3(e: Exception) extends RuntimeException(e)
1213

1314
"scoped" should "throw the exception thrown by a joined fork" in {
1415
val trail = Trail()
@@ -72,14 +73,41 @@ class ExceptionTest extends AnyFlatSpec with Matchers {
7273
throw CustomException()
7374
}
7475
}
75-
catch
76-
case e: Exception =>
77-
val suppressed = e.getSuppressed.map(_.getClass.getSimpleName)
78-
trail.add(s"${e.getClass.getSimpleName}(suppressed=${suppressed.mkString(",")})")
76+
catch case e: Exception => addExceptionWithSuppressedTo(trail, e)
7977

8078
trail.get shouldBe Vector("CustomException(suppressed=CustomException2)")
8179
}
8280

81+
it should "not add the original exception as suppressed" in {
82+
val trail = Trail()
83+
try
84+
supervised {
85+
val f = fork {
86+
throw new CustomException()
87+
}
88+
f.join()
89+
}
90+
catch case e: Exception => addExceptionWithSuppressedTo(trail, e)
91+
92+
trail.get shouldBe Vector("CustomException(suppressed=)")
93+
}
94+
95+
it should "add an exception as suppressed, even if it wraps the original exception" in {
96+
val trail = Trail()
97+
try
98+
supervised {
99+
val f = fork {
100+
throw new CustomException()
101+
}
102+
try f.join() catch {
103+
case e: Exception => throw new CustomException3(e)
104+
}
105+
}
106+
catch case e: Exception => addExceptionWithSuppressedTo(trail, e)
107+
108+
trail.get shouldBe Vector("CustomException(suppressed=CustomException3)")
109+
}
110+
83111
"joinEither" should "catch the exception with which a fork ends" in {
84112
val r = supervised {
85113
val f = forkUnsupervised {
@@ -90,4 +118,9 @@ class ExceptionTest extends AnyFlatSpec with Matchers {
90118

91119
r should matchPattern { case Left(e: CustomException) => }
92120
}
121+
122+
def addExceptionWithSuppressedTo(t: Trail, e: Throwable): Unit = {
123+
val suppressed = e.getSuppressed.map(_.getClass.getSimpleName)
124+
t.add(s"${e.getClass.getSimpleName}(suppressed=${suppressed.mkString(",")})")
125+
}
93126
}

0 commit comments

Comments
 (0)