Skip to content

Commit 8b54dbc

Browse files
authored
Merge pull request #244 from retronym/ticket/243
Avoid catching fatal errors as a Failed future
2 parents 7c494a7 + 206e2a3 commit 8b54dbc

File tree

2 files changed

+50
-5
lines changed

2 files changed

+50
-5
lines changed

src/main/scala/scala/async/FutureStateMachine.scala

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import java.util.Objects
1515

1616
import scala.util.{Failure, Success, Try}
1717
import scala.concurrent.{ExecutionContext, Future, Promise}
18+
import scala.util.control.NonFatal
1819

1920
/** The base class for state machines generated by the `scala.async.Async.async` macro.
2021
* Not intended to be directly extended in user-written code.
@@ -34,12 +35,14 @@ abstract class FutureStateMachine(execContext: ExecutionContext) extends Functio
3435
/** Assign `i` to the state variable */
3536
protected def state_=(s: Int): Unit = state$async = s
3637

38+
NonFatal // eagerly classloading NonFatal to reduce the chance of a cascading StackOverflowError in `completeFailure`
39+
3740
/** Complete the state machine with the given failure. */
38-
// scala-async accidentally started catching NonFatal exceptions in:
39-
// https://github.com/scala/scala-async/commit/e3ff0382ae4e015fc69da8335450718951714982#diff-136ab0b6ecaee5d240cd109e2b17ccb2R411
40-
// This follows the new behaviour but should we fix the regression?
41-
protected def completeFailure(t: Throwable): Unit = {
42-
result$async.complete(Failure(t))
41+
protected def completeFailure(t: Throwable): Unit = t match {
42+
case NonFatal(t) =>
43+
result$async.complete(Failure(t))
44+
case _ =>
45+
throw t
4346
}
4447

4548
/** Complete the state machine with the given value. */
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package scala.async
2+
3+
import java.util.concurrent.atomic.AtomicReference
4+
5+
import org.junit.{Assert, Ignore, Test}
6+
7+
import scala.async.Async.{async, await}
8+
import scala.concurrent.{ExecutionContext, Future, _}
9+
import scala.language.postfixOps
10+
import scala.util.control.NonFatal
11+
12+
class ExceptionalTest {
13+
@Test
14+
def nonFatalNotCaughtFutureCombinators(): Unit = {
15+
check { implicit ec =>
16+
Future.successful(42).map(x => (x, throw fatal))
17+
}
18+
}
19+
20+
@Test
21+
def nonFatalNotCaughtAsync(): Unit = {
22+
check { implicit ec =>
23+
async {
24+
(await(Future.successful(42)), throw fatal)
25+
}
26+
}
27+
}
28+
29+
def check(f: ExecutionContext => Future[Any]): Unit = {
30+
val lastUncaught = new AtomicReference[Throwable]()
31+
implicit val executor: ExecutionContextExecutor = ExecutionContext.fromExecutor(null, lastUncaught.set(_))
32+
val future = f(executor)
33+
Thread.sleep(100)
34+
Assert.assertSame(fatal, lastUncaught.get())
35+
}
36+
37+
private val fatal: Throwable = {
38+
val t = new VirtualMachineError() {}
39+
Assert.assertTrue(NonFatal.unapply(t).isEmpty)
40+
t
41+
}
42+
}

0 commit comments

Comments
 (0)