From 8d70133170a0eeea46a3f6e38c0a6ab7d06183fc Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Dec 2022 04:01:49 +0000 Subject: [PATCH 1/9] Correctly handle exit cases in `Resource#both` --- .../scala/cats/effect/kernel/Resource.scala | 79 +++++++++++++++---- .../test/scala/cats/effect/ResourceSpec.scala | 78 ++++++++++++++++++ 2 files changed, 143 insertions(+), 14 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 48ea1efe16..c9ef41da8e 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala @@ -280,21 +280,72 @@ sealed abstract class Resource[F[_], +A] extends Serializable { */ def both[B]( that: Resource[F, B] - )(implicit F: Concurrent[F]): Resource[F, (A, B)] = { - type Update = (F[Unit] => F[Unit]) => F[Unit] - - def allocate[C](r: Resource[F, C], storeFinalizer: Update): F[C] = - r.fold(_.pure[F], release => storeFinalizer(F.guarantee(_, release))) - - val bothFinalizers = F.ref(F.unit -> F.unit) - - Resource.make(bothFinalizers)(_.get.flatMap(_.parTupled).void).evalMap { store => - val thisStore: Update = f => store.update(_.bimap(f, identity)) - val thatStore: Update = f => store.update(_.bimap(identity, f)) + )(implicit F: Concurrent[F]): Resource[F, (A, B)] = + Resource + .makeCaseFull[F, ((A, ExitCase => F[Unit]), (B, ExitCase => F[Unit]))] { + (poll: Poll[F]) => + poll(F.racePair(this.allocatedCase, that.allocatedCase)).flatMap { + case Left((oc, f)) => + def cancelRight(ec: ExitCase): F[Unit] = + f.cancel *> f.join.flatMap(_.fold(F.unit, _ => F.unit, _.flatMap(_._2(ec)))) + + oc match { + case Outcome.Succeeded(fa) => + poll(f.join) + .onCancel( + F.void( + F.both( + fa.flatMap(_._2(ExitCase.Canceled)), + cancelRight(ExitCase.Canceled) + ) + ) + ) + .flatMap { + case Outcome.Succeeded(fb) => F.product(fa, fb) + case Outcome.Errored(ex) => + fa.flatMap(_._2(ExitCase.Errored(ex))) *> F.raiseError(ex) + case Outcome.Canceled() => + fa.flatMap(_._2(ExitCase.Canceled)) *> F.canceled *> poll(F.never) + } + case Outcome.Errored(ex) => + cancelRight(ExitCase.Errored(ex)) *> F.raiseError(ex) + case Outcome.Canceled() => + cancelRight(ExitCase.Canceled) *> F.canceled *> poll(F.never) + } + case Right((f, oc)) => + def cancelLeft(ec: ExitCase): F[Unit] = + f.cancel *> f.join.flatMap(_.fold(F.unit, _ => F.unit, _.flatMap(_._2(ec)))) + + oc match { + case Outcome.Succeeded(fb) => + poll(f.join) + .onCancel( + F.void( + F.both( + fb.flatMap(_._2(ExitCase.Canceled)), + cancelLeft(ExitCase.Canceled) + ) + ) + ) + .flatMap { + case Outcome.Succeeded(fa) => F.product(fa, fb) + case Outcome.Errored(ex) => + fb.flatMap(_._2(ExitCase.Errored(ex))) *> F.raiseError(ex) + case Outcome.Canceled() => + fb.flatMap(_._2(ExitCase.Canceled)) *> F.canceled *> poll(F.never) + } + case Outcome.Errored(ex) => + cancelLeft(ExitCase.Errored(ex)) *> F.raiseError(ex) + case Outcome.Canceled() => + cancelLeft(ExitCase.Canceled) *> F.canceled *> poll(F.never) + } - (allocate(this, thisStore), allocate(that, thatStore)).parTupled - } - } + } + } { + case (((_, releaseLeft), (_, releaseRight)), ec) => + F.void(F.both(releaseLeft(ec), releaseRight(ec))) + } + .map { case ((a, _), (b, _)) => (a, b) } /** * Races the evaluation of two resource allocations and returns the result of the winner, diff --git a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala index 565b0aa043..25dd8bee9f 100644 --- a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala @@ -598,6 +598,84 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { leftReleased must beTrue rightReleased must beTrue } + + "passes along the exit case" in ticked { implicit ticker => + import Resource.ExitCase + + { // use successfully, test left + var got: ExitCase = null + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + r.both(Resource.unit).use(_ => IO.unit) must completeAs(()) + got mustEqual ExitCase.Succeeded + } + + { // use successfully, test right + var got: ExitCase = null + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + Resource.unit.both(r).use(_ => IO.unit) must completeAs(()) + got mustEqual ExitCase.Succeeded + } + + { // use errored, test left + var got: ExitCase = null + val ex = new Exception + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + r.both(Resource.unit).use(_ => IO.raiseError(ex)) must failAs(ex) + got mustEqual ExitCase.Errored(ex) + } + + { // use errored, test right + var got: ExitCase = null + val ex = new Exception + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + Resource.unit.both(r).use(_ => IO.raiseError(ex)) must failAs(ex) + got mustEqual ExitCase.Errored(ex) + } + + { // right errored, test left + var got: ExitCase = null + val ex = new Exception + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + r.both(Resource.eval(IO.sleep(1.second) *> IO.raiseError(ex))).use_ must failAs(ex) + got mustEqual ExitCase.Errored(ex) + } + + { // left errored, test right + var got: ExitCase = null + val ex = new Exception + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + Resource.eval(IO.sleep(1.second) *> IO.raiseError(ex)).both(r).use_ must failAs(ex) + got mustEqual ExitCase.Errored(ex) + } + + { // use canceled, test left + var got: ExitCase = null + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + r.both(Resource.unit).use(_ => IO.canceled) must selfCancel + got mustEqual ExitCase.Canceled + } + + { // use canceled, test right + var got: ExitCase = null + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + Resource.unit.both(r).use(_ => IO.canceled) must selfCancel + got mustEqual ExitCase.Canceled + } + + { // right canceled, test left + var got: ExitCase = null + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + r.both(Resource.eval(IO.sleep(1.second) *> IO.canceled)).use_ must selfCancel + got mustEqual ExitCase.Canceled + } + + { // left canceled, test right + var got: ExitCase = null + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + Resource.eval(IO.sleep(1.second) *> IO.canceled).both(r).use_ must selfCancel + got mustEqual ExitCase.Canceled + } + } } "releases both resources on combineK" in ticked { implicit ticker => From 1a37a02abc409e94550f8399f90a97a7cbff8ffa Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Dec 2022 04:35:04 +0000 Subject: [PATCH 2/9] Revert changes to `Resource#both` --- .../scala/cats/effect/kernel/Resource.scala | 79 ++++--------------- 1 file changed, 14 insertions(+), 65 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 c9ef41da8e..48ea1efe16 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala @@ -280,72 +280,21 @@ sealed abstract class Resource[F[_], +A] extends Serializable { */ def both[B]( that: Resource[F, B] - )(implicit F: Concurrent[F]): Resource[F, (A, B)] = - Resource - .makeCaseFull[F, ((A, ExitCase => F[Unit]), (B, ExitCase => F[Unit]))] { - (poll: Poll[F]) => - poll(F.racePair(this.allocatedCase, that.allocatedCase)).flatMap { - case Left((oc, f)) => - def cancelRight(ec: ExitCase): F[Unit] = - f.cancel *> f.join.flatMap(_.fold(F.unit, _ => F.unit, _.flatMap(_._2(ec)))) - - oc match { - case Outcome.Succeeded(fa) => - poll(f.join) - .onCancel( - F.void( - F.both( - fa.flatMap(_._2(ExitCase.Canceled)), - cancelRight(ExitCase.Canceled) - ) - ) - ) - .flatMap { - case Outcome.Succeeded(fb) => F.product(fa, fb) - case Outcome.Errored(ex) => - fa.flatMap(_._2(ExitCase.Errored(ex))) *> F.raiseError(ex) - case Outcome.Canceled() => - fa.flatMap(_._2(ExitCase.Canceled)) *> F.canceled *> poll(F.never) - } - case Outcome.Errored(ex) => - cancelRight(ExitCase.Errored(ex)) *> F.raiseError(ex) - case Outcome.Canceled() => - cancelRight(ExitCase.Canceled) *> F.canceled *> poll(F.never) - } - case Right((f, oc)) => - def cancelLeft(ec: ExitCase): F[Unit] = - f.cancel *> f.join.flatMap(_.fold(F.unit, _ => F.unit, _.flatMap(_._2(ec)))) - - oc match { - case Outcome.Succeeded(fb) => - poll(f.join) - .onCancel( - F.void( - F.both( - fb.flatMap(_._2(ExitCase.Canceled)), - cancelLeft(ExitCase.Canceled) - ) - ) - ) - .flatMap { - case Outcome.Succeeded(fa) => F.product(fa, fb) - case Outcome.Errored(ex) => - fb.flatMap(_._2(ExitCase.Errored(ex))) *> F.raiseError(ex) - case Outcome.Canceled() => - fb.flatMap(_._2(ExitCase.Canceled)) *> F.canceled *> poll(F.never) - } - case Outcome.Errored(ex) => - cancelLeft(ExitCase.Errored(ex)) *> F.raiseError(ex) - case Outcome.Canceled() => - cancelLeft(ExitCase.Canceled) *> F.canceled *> poll(F.never) - } + )(implicit F: Concurrent[F]): Resource[F, (A, B)] = { + type Update = (F[Unit] => F[Unit]) => F[Unit] - } - } { - case (((_, releaseLeft), (_, releaseRight)), ec) => - F.void(F.both(releaseLeft(ec), releaseRight(ec))) - } - .map { case ((a, _), (b, _)) => (a, b) } + def allocate[C](r: Resource[F, C], storeFinalizer: Update): F[C] = + r.fold(_.pure[F], release => storeFinalizer(F.guarantee(_, release))) + + val bothFinalizers = F.ref(F.unit -> F.unit) + + Resource.make(bothFinalizers)(_.get.flatMap(_.parTupled).void).evalMap { store => + val thisStore: Update = f => store.update(_.bimap(f, identity)) + val thatStore: Update = f => store.update(_.bimap(identity, f)) + + (allocate(this, thisStore), allocate(that, thatStore)).parTupled + } + } /** * Races the evaluation of two resource allocations and returns the result of the winner, From d82fba15bed9a34dc4246e0b01af94915a1f03aa Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Dec 2022 05:27:07 +0000 Subject: [PATCH 3/9] Handle `ExitCase` in `Resource#fold`, fix `both`, `combineK` --- .../scala/cats/effect/kernel/Resource.scala | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 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 48ea1efe16..67583c29c2 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala @@ -154,7 +154,7 @@ sealed abstract class Resource[F[_], +A] extends Serializable { private[effect] def fold[B]( onOutput: A => F[B], - onRelease: F[Unit] => F[Unit] + onRelease: (ExitCase => F[Unit], ExitCase) => F[Unit] )(implicit F: MonadCancel[F, Throwable]): F[B] = { sealed trait Stack[AA] case object Nil extends Stack[A] @@ -178,7 +178,7 @@ sealed abstract class Resource[F[_], +A] extends Serializable { } } { case ((_, release), outcome) => - onRelease(release(ExitCase.fromOutcome(outcome))) + onRelease(release, ExitCase.fromOutcome(outcome)) } case Bind(source, fs) => loop(source, Frame(fs, stack)) @@ -204,7 +204,7 @@ sealed abstract class Resource[F[_], +A] extends Serializable { * the result of applying [F] to */ def use[B](f: A => F[B])(implicit F: MonadCancel[F, Throwable]): F[B] = - fold(f, identity) + fold(f, _.apply(_)) /** * For a resource that allocates an action (type `F[B]`), allocate that action, run it and @@ -281,19 +281,31 @@ sealed abstract class Resource[F[_], +A] extends Serializable { def both[B]( that: Resource[F, B] )(implicit F: Concurrent[F]): Resource[F, (A, B)] = { - type Update = (F[Unit] => F[Unit]) => F[Unit] + type Finalizer = Resource.ExitCase => F[Unit] + type Update = (Finalizer => Finalizer) => F[Unit] def allocate[C](r: Resource[F, C], storeFinalizer: Update): F[C] = - r.fold(_.pure[F], release => storeFinalizer(F.guarantee(_, release))) - - val bothFinalizers = F.ref(F.unit -> F.unit) - - Resource.make(bothFinalizers)(_.get.flatMap(_.parTupled).void).evalMap { store => - val thisStore: Update = f => store.update(_.bimap(f, identity)) - val thatStore: Update = f => store.update(_.bimap(identity, f)) + r.fold( + _.pure[F], + (release, _) => storeFinalizer(fin => ec => F.unit >> fin(ec).guarantee(release(ec))) + ) + + val noop: Finalizer = _ => F.unit + val bothFinalizers = F.ref((noop, noop)) + + Resource + .makeCase(bothFinalizers) { (finalizers, ec) => + finalizers.get.flatMap { + case (thisFin, thatFin) => + F.void(F.both(thisFin(ec), thatFin(ec))) + } + } + .evalMap { store => + val thisStore: Update = f => store.update(_.bimap(f, identity)) + val thatStore: Update = f => store.update(_.bimap(identity, f)) - (allocate(this, thisStore), allocate(that, thatStore)).parTupled - } + F.both(allocate(this, thisStore), allocate(that, thatStore)) + } } /** @@ -661,12 +673,19 @@ sealed abstract class Resource[F[_], +A] extends Serializable { implicit F: MonadCancel[F, Throwable], K: SemigroupK[F], G: Ref.Make[F]): Resource[F, B] = - Resource.make(Ref[F].of(F.unit))(_.get.flatten).evalMap { finalizers => - def allocate(r: Resource[F, B]): F[B] = - r.fold(_.pure[F], (release: F[Unit]) => finalizers.update(_.guarantee(release))) - - K.combineK(allocate(this), allocate(that)) - } + Resource + .makeCase(Ref[F].of((_: Resource.ExitCase) => F.unit))((fin, ec) => + fin.get.flatMap(_(ec))) + .evalMap { finalizers => + def allocate(r: Resource[F, B]): F[B] = + r.fold( + _.pure[F], + (release, _) => + finalizers.update(fin => ec => F.unit >> fin(ec).guarantee(release(ec))) + ) + + K.combineK(allocate(this), allocate(that)) + } } From 605dccf35f49bfc5e4c4c61a5c459fb24085f0a4 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Dec 2022 05:33:06 +0000 Subject: [PATCH 4/9] Split into smaller tests --- .../test/scala/cats/effect/ResourceSpec.scala | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala index 25dd8bee9f..5cb7d9b2ea 100644 --- a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala @@ -599,24 +599,24 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { rightReleased must beTrue } - "passes along the exit case" in ticked { implicit ticker => + "passes along the exit case" in { import Resource.ExitCase - { // use successfully, test left + "use succesfully, test left" >> ticked { implicit ticker => var got: ExitCase = null val r = Resource.onFinalizeCase(ec => IO { got = ec }) r.both(Resource.unit).use(_ => IO.unit) must completeAs(()) got mustEqual ExitCase.Succeeded } - { // use successfully, test right + "use successfully, test right" >> ticked { implicit ticker => var got: ExitCase = null val r = Resource.onFinalizeCase(ec => IO { got = ec }) Resource.unit.both(r).use(_ => IO.unit) must completeAs(()) got mustEqual ExitCase.Succeeded } - { // use errored, test left + "use errored, test left" >> ticked { implicit ticker => var got: ExitCase = null val ex = new Exception val r = Resource.onFinalizeCase(ec => IO { got = ec }) @@ -624,7 +624,7 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { got mustEqual ExitCase.Errored(ex) } - { // use errored, test right + "use errored, test right" >> ticked { implicit ticker => var got: ExitCase = null val ex = new Exception val r = Resource.onFinalizeCase(ec => IO { got = ec }) @@ -632,7 +632,7 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { got mustEqual ExitCase.Errored(ex) } - { // right errored, test left + "right errored, test left" >> ticked { implicit ticker => var got: ExitCase = null val ex = new Exception val r = Resource.onFinalizeCase(ec => IO { got = ec }) @@ -640,7 +640,7 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { got mustEqual ExitCase.Errored(ex) } - { // left errored, test right + "left errored, test right" >> ticked { implicit ticker => var got: ExitCase = null val ex = new Exception val r = Resource.onFinalizeCase(ec => IO { got = ec }) @@ -648,28 +648,28 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { got mustEqual ExitCase.Errored(ex) } - { // use canceled, test left + "use canceled, test left" >> ticked { implicit ticker => var got: ExitCase = null val r = Resource.onFinalizeCase(ec => IO { got = ec }) r.both(Resource.unit).use(_ => IO.canceled) must selfCancel got mustEqual ExitCase.Canceled } - { // use canceled, test right + "use canceled, test right" >> ticked { implicit ticker => var got: ExitCase = null val r = Resource.onFinalizeCase(ec => IO { got = ec }) Resource.unit.both(r).use(_ => IO.canceled) must selfCancel got mustEqual ExitCase.Canceled } - { // right canceled, test left + "right canceled, test left" >> ticked { implicit ticker => var got: ExitCase = null val r = Resource.onFinalizeCase(ec => IO { got = ec }) r.both(Resource.eval(IO.sleep(1.second) *> IO.canceled)).use_ must selfCancel got mustEqual ExitCase.Canceled } - { // left canceled, test right + "left canceled, test right" >> ticked { implicit ticker => var got: ExitCase = null val r = Resource.onFinalizeCase(ec => IO { got = ec }) Resource.eval(IO.sleep(1.second) *> IO.canceled).both(r).use_ must selfCancel From d2d27d67b0b40ab476a9e0851757dce605ec5372 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Dec 2022 06:01:50 +0000 Subject: [PATCH 5/9] Add `ExitCase` tests for `Resource#combineK` --- .../test/scala/cats/effect/ResourceSpec.scala | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala index 5cb7d9b2ea..ab67d2ca3d 100644 --- a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala @@ -720,6 +720,49 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { lhs eqv rhs } } + + "passes along the exit case" in { + import Resource.ExitCase + + "use succesfully, test left" >> ticked { implicit ticker => + var got: ExitCase = null + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + r.combineK(Resource.unit).use(_ => IO.unit) must completeAs(()) + got mustEqual ExitCase.Succeeded + } + + "use errored, test left" >> ticked { implicit ticker => + var got: ExitCase = null + val ex = new Exception + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + r.combineK(Resource.unit).use(_ => IO.raiseError(ex)) must failAs(ex) + got mustEqual ExitCase.Errored(ex) + } + + "left errored, test left" >> ticked { implicit ticker => + var got: ExitCase = null + val ex = new Exception + val r = Resource.onFinalizeCase(ec => IO { got = ec }) *> + Resource.eval(IO.raiseError(ex)) + r.combineK(Resource.unit).use_ must completeAs(()) + got mustEqual ExitCase.Succeeded + } + + "left errored, test right" >> ticked { implicit ticker => + var got: ExitCase = null + val ex = new Exception + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + Resource.eval(IO.raiseError(ex)).combineK(r).use_ must completeAs(()) + got mustEqual ExitCase.Succeeded + } + + "use canceled, test left" >> ticked { implicit ticker => + var got: ExitCase = null + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + r.combineK(Resource.unit).use(_ => IO.canceled) must selfCancel + got mustEqual ExitCase.Canceled + } + } } "surround" should { From 9e8d092ea210e45ddd2269683ab1448f7f292c90 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Tue, 6 Dec 2022 06:05:44 +0000 Subject: [PATCH 6/9] Fix awk wording --- tests/shared/src/test/scala/cats/effect/ResourceSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala index ab67d2ca3d..0fa421c925 100644 --- a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala @@ -599,7 +599,7 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { rightReleased must beTrue } - "passes along the exit case" in { + "propagate the exit case" in { import Resource.ExitCase "use succesfully, test left" >> ticked { implicit ticker => @@ -721,7 +721,7 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { } } - "passes along the exit case" in { + "propagate the exit case" in { import Resource.ExitCase "use succesfully, test left" >> ticked { implicit ticker => From 484a9846f8d385c673474916d51dd1c671af05b0 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 24 Dec 2022 03:01:43 +0000 Subject: [PATCH 7/9] Document `ExitCase` semantics for `Resource#both` --- .../shared/src/main/scala/cats/effect/kernel/Resource.scala | 4 ++++ 1 file changed, 4 insertions(+) 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 67583c29c2..021a755bab 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala @@ -251,6 +251,10 @@ sealed abstract class Resource[F[_], +A] extends Serializable { * _each_ of the two resources, nested finalizers are run in the usual reverse order of * acquisition. * + * The same [[ExitCase]] is propagated to every finalizer. If both resources acquired + * successfully, the [[ExitCase]] is determined by the outcome of [[use]]. Otherwise, it is + * determined by which resource failed or canceled first during acquisition. + * * Note that `Resource` also comes with a `cats.Parallel` instance that offers more convenient * access to the same functionality as `both`, for example via `parMapN`: * From d1f0397fa3d27b9b3d6e17287ed4ea1ad01e9379 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 24 Dec 2022 03:19:16 +0000 Subject: [PATCH 8/9] Add more test cases for r `Resource#combineK` --- .../test/scala/cats/effect/ResourceSpec.scala | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala index 0fa421c925..d23548d324 100644 --- a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala @@ -756,12 +756,33 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { got mustEqual ExitCase.Succeeded } + "left errored, use errored, test right" >> ticked { implicit ticker => + var got: ExitCase = null + val ex = new Exception + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + Resource + .eval(IO.raiseError(new Exception)) + .combineK(r) + .use(_ => IO.raiseError(ex)) must failAs(ex) + got mustEqual ExitCase.Errored(ex) + } + "use canceled, test left" >> ticked { implicit ticker => var got: ExitCase = null val r = Resource.onFinalizeCase(ec => IO { got = ec }) r.combineK(Resource.unit).use(_ => IO.canceled) must selfCancel got mustEqual ExitCase.Canceled } + + "left errored, use canceled, test right" >> ticked { implicit ticker => + var got: ExitCase = null + val r = Resource.onFinalizeCase(ec => IO { got = ec }) + Resource + .eval(IO.raiseError(new Exception)) + .combineK(r) + .use(_ => IO.canceled) must selfCancel + got mustEqual ExitCase.Canceled + } } } From be51ea6474184384b9fcc55d736e48f1e42b8c01 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sat, 24 Dec 2022 03:54:49 +0000 Subject: [PATCH 9/9] Fix scaladoc linking --- .../shared/src/main/scala/cats/effect/kernel/Resource.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 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 021a755bab..6acb70e942 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala @@ -251,9 +251,9 @@ sealed abstract class Resource[F[_], +A] extends Serializable { * _each_ of the two resources, nested finalizers are run in the usual reverse order of * acquisition. * - * The same [[ExitCase]] is propagated to every finalizer. If both resources acquired - * successfully, the [[ExitCase]] is determined by the outcome of [[use]]. Otherwise, it is - * determined by which resource failed or canceled first during acquisition. + * The same [[Resource.ExitCase]] is propagated to every finalizer. If both resources acquired + * successfully, the [[Resource.ExitCase]] is determined by the outcome of [[use]]. Otherwise, + * it is determined by which resource failed or canceled first during acquisition. * * Note that `Resource` also comes with a `cats.Parallel` instance that offers more convenient * access to the same functionality as `both`, for example via `parMapN`: