The current implementation of bracketState does not guarantee that:
use will be cancelled when fiber is cancelled
commit will be executed if fa succeeds
The following code demonstrates the problem:
import cats.effect._
import java.time.Instant
import scala.concurrent.duration._
import tofu.syntax.bracket._
object Demo extends IOApp {
def log(text: String): IO[Unit] = IO(println(s"${Instant.now}: $text"))
val slow: IO[Unit] = for {
_ <- log("start")
_ <- Timer[IO].sleep(3.seconds)
_ <- log("finish")
} yield ()
override def run(args: List[String]): IO[ExitCode] =
slow.bracketState { _ =>
log("use").as((), ())
} { _ =>
log("commit")
}.timeoutTo(1.second, log("timeout"))
.as(ExitCode.Success)
}
The output looks like this:
2021-12-09T17:18:30.869187693Z: start
2021-12-09T17:18:33.897543168Z: finish
2021-12-09T17:18:33.899531757Z: use
2021-12-09T17:18:33.900238459Z: timeout
The "finish" message is still printed although the fiber is cancelled by timeoutTo. This is because the first FG.bracket call in bracketState code makes its init block uncancellable. The "commit" message is not printed because bracketCase calls it in the use block of the first FG.bracket, but it is not guaranteed that use will be executed after successful init.
This behavior breaks bracketModify and MVar-based tofu.memo.Cached.
The current implementation of
bracketStatedoes not guarantee that:usewill be cancelled when fiber is cancelledcommitwill be executed iffasucceedsThe following code demonstrates the problem:
The output looks like this:
The "finish" message is still printed although the fiber is cancelled by
timeoutTo. This is because the firstFG.bracketcall inbracketStatecode makes itsinitblock uncancellable. The "commit" message is not printed becausebracketCasecalls it in theuseblock of the firstFG.bracket, but it is not guaranteed thatusewill be executed after successfulinit.This behavior breaks
bracketModifyandMVar-basedtofu.memo.Cached.