From 3af09e6d563a1496f01033d7ab321efad588a0ab Mon Sep 17 00:00:00 2001 From: Justin Reardon Date: Sat, 4 Jul 2026 21:45:22 -0400 Subject: [PATCH] Fix #4627 resource early cancelation Adds uncancelable wrapper to evaluation of resources in allocatedCase so that cancelation can only occur at reasonable points. --- .../scala/cats/effect/kernel/Resource.scala | 63 +++++++++---------- .../scala/cats/effect/ResourceSuite.scala | 20 ++++++ 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala index be7090bfa3..1bc34abed0 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala @@ -467,23 +467,22 @@ sealed abstract class Resource[F[_], +A] extends Serializable { case object Nil extends Stack[B] final case class Frame[AA, BB](head: AA => Resource[F, BB], tail: Stack[BB]) extends Stack[AA] - - // Indirection for calling `loop` needed because `loop` must be @tailrec - def continue[C]( - current: Resource[F, C], - stack: Stack[C], - release: ExitCase => F[Unit]): F[(B, ExitCase => F[Unit])] = - loop(current, stack, release) - - // Interpreter that knows how to evaluate a Resource data structure; - // Maintains its own stack for dealing with Bind chains - @tailrec def loop[C]( - current: Resource[F, C], - stack: Stack[C], - release: ExitCase => F[Unit]): F[(B, ExitCase => F[Unit])] = - current match { - case Allocate(resource) => - F uncancelable { poll => + F uncancelable { poll => + // Indirection for calling `loop` needed because `loop` must be @tailrec + def continue[C]( + current: Resource[F, C], + stack: Stack[C], + release: ExitCase => F[Unit]): F[(B, ExitCase => F[Unit])] = + loop(current, stack, release) + + // Interpreter that knows how to evaluate a Resource data structure; + // Maintains its own stack for dealing with Bind chains + @tailrec def loop[C]( + current: Resource[F, C], + stack: Stack[C], + release: ExitCase => F[Unit]): F[(B, ExitCase => F[Unit])] = + current match { + case Allocate(resource) => resource(poll) flatMap { case (b, rel) => // Insert F.unit to emulate defer for stack-safety @@ -505,29 +504,29 @@ sealed abstract class Resource[F[_], +A] extends Serializable { F.pure((b, rel2)) case Frame(head, tail) => - poll(continue(head(b), tail, rel2)) + (poll(F.unit) >> continue(head(b), tail, rel2)) .onCancel(rel(ExitCase.Canceled)) .onError { case e => rel(ExitCase.Errored(e)).handleError(_ => ()) } } } - } - case Bind(source, fs) => - loop(source, Frame(fs, stack), release) + case Bind(source, fs) => + loop(source, Frame(fs, stack), release) - case Pure(v) => - stack match { - case Nil => - (v: B, release).pure[F] - case Frame(head, tail) => - loop(head(v), tail, release) - } + case Pure(v) => + stack match { + case Nil => + (v: B, release).pure[F] + case Frame(head, tail) => + loop(head(v), tail, release) + } - case Eval(fa) => - fa.flatMap(a => continue(Resource.pure(a), stack, release)) - } + case Eval(fa) => + poll(fa).flatMap(a => continue(Resource.pure(a), stack, release)) + } - loop(this, Nil, _ => F.unit) + loop(this, Nil, _ => F.unit) + } } /** diff --git a/tests/shared/src/test/scala/cats/effect/ResourceSuite.scala b/tests/shared/src/test/scala/cats/effect/ResourceSuite.scala index 4d187538cc..e06a47a057 100644 --- a/tests/shared/src/test/scala/cats/effect/ResourceSuite.scala +++ b/tests/shared/src/test/scala/cats/effect/ResourceSuite.scala @@ -154,6 +154,26 @@ class ResourceSuite extends BaseScalaCheckSuite with DisciplineSuite { forAll { (fa: IO[String]) => assertEqv(Resource.eval(fa).use(IO.pure), fa) } } + real("eval - uncancelable timeout is not canceled") { + Resource.eval(IO.uncancelable(_ => IO.sleep(100.millis))).timeout(10.millis).use_ + } + + real("eval - uncancelable continuation") { + val res = Resource + .make(IO.pure(42))(_ => IO.unit) + .flatMap(_ => Resource.eval(IO.uncancelable { _ => IO.canceled })) + + for { + ctr <- IO.ref(0) + fib <- IO.uncancelable { poll => + poll(res.allocatedCase).flatMap { _ => ctr.update(_ + 1) } + }.start + _ <- fib.join + c <- ctr.get + _ <- IO { assertEquals(c, 1) } + } yield () + } + ticked("eval - interruption") { implicit ticker => def resource(d: Deferred[IO, Int]): Resource[IO, Unit] = for {