zio / zio

ZIO — A type-safe, composable library for async and concurrent programming in Scala
https://zio.dev
Apache License 2.0
4.07k stars 1.28k forks source link

`foldCauseZIO` provides interruptions to the failure case #7211

Closed quelgar closed 2 years ago

quelgar commented 2 years ago

Should this test pass?

      test("does not peek at interruptions") {
        for {
          ref    <- Ref.make(false)
          result <- ZIO.interrupt.tapDefect(_ => ref.set(true)).exit
          effect <- ref.get
        } yield assert(result)(isInterrupted) && assert(effect)(isFalse)
      }
    - ZIOSpec / tapDefect / does not peek at interruptions
      ✗ true was not false
      effect did not satisfy isFalse
      effect = true

tapDefect is built on foldCauseZIO, the Scaladoc of which I interpret as meaning interruptions will not be passed to the failure case, but it appears they are passed to it.

adamgfraser commented 2 years ago

This is expected behavior. This is codified in the following test in the "interruption semantics" suite:

test("self-interruption can be averted") {
  for {
    ref   <- Ref.make(false)
    fiber <- ZIO.interrupt.catchAllCause(_ => ref.set(true)).fork
    _     <- fiber.await
    value <- ref.get
  } yield assertTrue(value == true)
}
quelgar commented 2 years ago

Yes, I'd expect catchAllCause to catch all causes. But tapDefect filters out failures and I'd expected it to filter out interruptions too.

I think the Scaladoc of foldCauseZIO is misleading then? "Except interruptions" should be removed.

  /**
   * A more powerful version of `foldZIO` that allows recovering from any kind
   * of failure except interruptions.
   */
  final def foldCauseZIO[R1 <: R, E2, B](
scala> val w = ZIO.interrupt.foldCause(c => s"failure: ${c.isInterrupted}", _ => "success")
val w: zio.URIO[Any, String] = OnSuccessAndFailure(<empty>.rs$line$21$.w.macro(rs$line$21:1),Stateful(<empty>.rs$line$21$.w.macro(rs$line$21:1),zio.ZIO$$$Lambda$1414/0x00000008005e5f28@11476fd1),zio.ZIO$$Lambda$1999/0x00000008007a27d8@14b26bc2,zio.ZIO$$Lambda$1998/0x00000008007a21b8@5ed3696c)

scala> Unsafe.unsafe(Runtime.default.unsafe.run(w))
val res8: zio.Exit[Nothing, String] = Success(failure: true)
adamgfraser commented 2 years ago

A more precise statement would be that foldCauseZIO allows recovering from any kind of failure except external interruptions. To see this consider the following test:

test("external interruption cannot be recovered from") {
  for {
    promise <- Promise.make[Nothing, Unit]
    ref     <- Ref.make(false)
    fiber <- ZIO.uninterruptibleMask { restore =>
                promise.succeed(()) *> restore(ZIO.never).foldCauseZIO(_ => ref.set(true), _ => ZIO.unit)
              }.forkDaemon
    _     <- promise.await
    exit  <- fiber.interrupt
    value <- ref.get
  } yield assert(value)(isTrue) && assert(exit)(isInterrupted)
}

In this example the fiber is externally interrupted and foldCauseZIO is able to observe that interruption and act on it assuming it is in an uninterruptible region. This is in fact how ensuring is implemented. However, foldCauseZIO cannot recover from the external interruption. Once the fiber is externally interrupted it is in wind down, which can be delayed while the fiber is in an uninterruptible region but can never be recovered from.

quelgar commented 2 years ago

Oh I see, thank you for explaining!