Skip to content

Commit

Permalink
Merge pull request #50 from dvgica/fix-swallowed-startup-exception
Browse files Browse the repository at this point in the history
Suppress teardown exceptions when setup exception already encountered
  • Loading branch information
dvgica authored Nov 25, 2021
2 parents 80a7687 + d3ca69c commit d284161
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 6 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ Once the `Managed` stack is composed, the underlying resources are built and use
Exception behavior is as follows:
- Exceptions during setup are thrown after already-built resources are torn down
- Exceptions during usage are thrown after resources are torn down
- Exceptions during teardown are thrown, but only after teardown is called on every resource. If an exception was thrown during usage, the teardown exceptions are added as suppressed exceptions on the usage exception.
- Exceptions during teardown are thrown, but only after teardown is called on every resource.
- If an exception is thrown during usage, and an exception occurred during teardown, the usage exception is thrown with the teardown exception added as a suppressed exception.
- If an exception is thrown during setup, and an exception occurred during teardown, the setup exception is thrown with the teardown exception added as a suppressed exception.

For more details, see the Scaladocs.

Expand Down
16 changes: 11 additions & 5 deletions managerial/src/main/scala/ca/dvgi/managerial/Managed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,21 @@ trait Managed[+T] { selfT =>
*/
def flatMap[U](f: T => Managed[U]): Managed[U] = new Managed[U] {
def build() = new Resource[U] {
val t = selfT.build()
private val t = selfT.build()

val u =
private val u =
try {
f(t.get).build()
} catch {
case e: Exception =>
t.teardown()
throw e
case setupThrowable: Throwable =>
try {
t.teardown()
} catch {
case teardownThrowable: Throwable =>
setupThrowable.addSuppressed(teardownThrowable)
}

throw setupThrowable
}

def get = u.get
Expand Down
29 changes: 29 additions & 0 deletions managerial/src/test/scala/ca/dvgi/managerial/ManagedTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -281,4 +281,33 @@ class ManagedTest extends munit.FunSuite {
assertEquals(r, i)
assert(tr.tornDown)
}

test(
"An exception in the Managed setup stack, followed by an exception in the teardown stack, surfaces the setup exception with a suppressed teardown exception"
) {
val setupException = new RuntimeException("setup exception")
val teardownException = new RuntimeException("teardown exception")

val m = for {
_ <- Managed.evalTeardown(throw teardownException)
_ <- Managed.evalSetup(throw setupException)
} yield ()

try {
m.build()
} catch {
case t: RuntimeException =>
assertEquals(t.getMessage, setupException.getMessage)
assertEquals(t.getSuppressed.size, 1)
t.getSuppressed()(0) match {
case te: RuntimeException =>
assertEquals(
te.getMessage,
teardownException.getMessage
)
case e => fail("Unexpected exception", e)
}
case _: Throwable => fail("Unexpected exception")
}
}
}

0 comments on commit d284161

Please sign in to comment.