From f3881a914c51be61ab878ebfc486dd53fd504c99 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Fri, 17 Feb 2023 18:22:34 +0100 Subject: [PATCH 01/11] Add parallel map2 test --- tests/shared/src/test/scala/cats/effect/IOSpec.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index ef82f9b998..8cc1048141 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -1273,6 +1273,17 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { (IO.raiseError[Unit](TestException), IO.never[Unit]).parTupled.void must failAs( TestException) } + + "short-circuit on canceled" in ticked { implicit ticker => + (IO.never[Unit], IO.canceled) + .parTupled + .start + .flatMap(_.join.map(_.isCanceled)) must completeAs(true) + (IO.canceled, IO.never[Unit]) + .parTupled + .start + .flatMap(_.join.map(_.isCanceled)) must completeAs(true) + } } "miscellaneous" should { From d7ee73f694370205663b08fc4b87fea1b972bc82 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Fri, 17 Feb 2023 18:38:12 +0100 Subject: [PATCH 02/11] The same test for PureConc --- .../src/test/scala/cats/effect/laws/PureConcSpec.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala b/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala index 966fb1c3ed..cc2842873a 100644 --- a/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala +++ b/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala @@ -48,6 +48,13 @@ class PureConcSpec extends Specification with Discipline with BaseSpec { pure.run((F.raiseError[Unit](42), F.never[Unit]).parTupled) mustEqual Outcome.Errored(42) } + "short-circuit on canceled" in { + pure.run((F.never[Unit], F.canceled).parTupled.start.flatMap(_.join)) mustEqual Outcome + .Succeeded(Some(Outcome.canceled[F, Nothing, Unit])) + pure.run((F.canceled, F.never[Unit]).parTupled.start.flatMap(_.join)) mustEqual Outcome + .Succeeded(Some(Outcome.canceled[F, Nothing, Unit])) + } + "not run forever on chained product" in { import cats.effect.kernel.Par.ParallelF From e86aef7b7ac7bfdbcc69aabc69f78c0b15d187b5 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Fri, 17 Feb 2023 19:51:49 +0100 Subject: [PATCH 03/11] Kick CI From 4393611545b1cb71d4beb9f494114884a7d612e8 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Fri, 17 Feb 2023 21:43:51 +0100 Subject: [PATCH 04/11] More test --- .../shared/src/test/scala/cats/effect/IOSpec.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index 8cc1048141..7d5afa8c8c 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -1284,6 +1284,20 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { .start .flatMap(_.join.map(_.isCanceled)) must completeAs(true) } + + "run finalizers when canceled" in ticked { implicit ticker => + val tsk = IO.ref(0).flatMap { ref => + val t = IO.never[Unit].onCancel(ref.update(_ + 1)) + for { + fib <- (t, t).parTupled.start + _ <- IO { ticker.ctx.tickAll() } + _ <- fib.cancel + c <- ref.get + } yield c + } + + tsk must completeAs(2) + } } "miscellaneous" should { From 5b2a8042780896a6d223473c980a44a6d1097b06 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Fri, 17 Feb 2023 23:22:24 +0100 Subject: [PATCH 05/11] More tests --- .../src/test/scala/cats/effect/IOSpec.scala | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index 7d5afa8c8c..2b05e0a405 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -1298,6 +1298,43 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { tsk must completeAs(2) } + + "run right side finalizer when canceled (and left side already completed)" in ticked { + implicit ticker => + val tsk = IO.ref(0).flatMap { ref => + for { + fib <- (IO.unit, IO.never[Unit].onCancel(ref.update(_ + 1))).parTupled.start + _ <- IO { ticker.ctx.tickAll() } + _ <- fib.cancel + c <- ref.get + } yield c + } + + tsk must completeAs(1) + } + + "run left side finalizer when canceled (and right side already completed)" in ticked { + implicit ticker => + val tsk = IO.ref(0).flatMap { ref => + for { + fib <- (IO.never[Unit].onCancel(ref.update(_ + 1)), IO.unit).parTupled.start + _ <- IO { ticker.ctx.tickAll() } + _ <- fib.cancel + c <- ref.get + } yield c + } + + tsk must completeAs(1) + } + + "complete if both sides complete" in ticked { implicit ticker => + val tsk = ( + IO.sleep(2.seconds).as(20), + IO.sleep(3.seconds).as(22) + ).parTupled.map { case (l, r) => l + r } + + tsk must completeAs(42) + } } "miscellaneous" should { From 4995c298c58ff2ef8788d5731813c89aea76b378 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Fri, 17 Feb 2023 23:45:14 +0100 Subject: [PATCH 06/11] Kick CI From 9e9d015514fbb482bdebe17aab3a68f1c7a95f93 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Fri, 17 Feb 2023 02:08:54 +0100 Subject: [PATCH 07/11] Go back to the old map2 implementation --- .../kernel/instances/GenSpawnInstances.scala | 140 +----------------- 1 file changed, 5 insertions(+), 135 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala b/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala index 0e7112a2ff..8dafcb049b 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala @@ -16,9 +16,9 @@ package cats.effect.kernel.instances -import cats.{~>, Align, Applicative, CommutativeApplicative, Eval, Functor, Monad, Parallel} +import cats.{~>, Align, Applicative, CommutativeApplicative, Functor, Monad, Parallel} import cats.data.Ior -import cats.effect.kernel.{GenSpawn, Outcome, ParallelF} +import cats.effect.kernel.{GenSpawn, ParallelF} import cats.implicits._ trait GenSpawnInstances { @@ -41,7 +41,6 @@ trait GenSpawnInstances { new (M ~> F) { def apply[A](ma: M[A]): F[A] = ParallelF[M, A](ma) } - } implicit def commutativeApplicativeForParallelF[F[_], E]( @@ -51,134 +50,9 @@ trait GenSpawnInstances { final override def pure[A](a: A): ParallelF[F, A] = ParallelF(F.pure(a)) final override def map2[A, B, Z](fa: ParallelF[F, A], fb: ParallelF[F, B])( - f: (A, B) => Z): ParallelF[F, Z] = - ParallelF( - F.uncancelable { poll => - for { - fiberA <- F.start(ParallelF.value(fa)) - fiberB <- F.start(ParallelF.value(fb)) - - // start a pair of supervisors to ensure that the opposite is canceled on error - _ <- F start { - fiberB.join flatMap { - case Outcome.Succeeded(_) => F.unit - case _ => fiberA.cancel - } - } - - _ <- F start { - fiberA.join flatMap { - case Outcome.Succeeded(_) => F.unit - case _ => fiberB.cancel - } - } - - a <- F - .onCancel(poll(fiberA.join), bothUnit(fiberA.cancel, fiberB.cancel)) - .flatMap[A] { - case Outcome.Succeeded(fa) => - fa - - case Outcome.Errored(e) => - fiberB.cancel *> F.raiseError(e) - - case Outcome.Canceled() => - fiberB.cancel *> poll { - fiberB.join flatMap { - case Outcome.Succeeded(_) | Outcome.Canceled() => - F.canceled *> F.never - case Outcome.Errored(e) => - F.raiseError(e) - } - } - } - - z <- F.onCancel(poll(fiberB.join), fiberB.cancel).flatMap[Z] { - case Outcome.Succeeded(fb) => - fb.map(b => f(a, b)) - - case Outcome.Errored(e) => - F.raiseError(e) - - case Outcome.Canceled() => - poll { - fiberA.join flatMap { - case Outcome.Succeeded(_) | Outcome.Canceled() => - F.canceled *> F.never - case Outcome.Errored(e) => - F.raiseError(e) - } - } - } - } yield z - } - ) - - final override def map2Eval[A, B, Z](fa: ParallelF[F, A], fb: Eval[ParallelF[F, B]])( - f: (A, B) => Z): Eval[ParallelF[F, Z]] = - Eval.now( - ParallelF( - F.uncancelable { poll => - for { - fiberA <- F.start(ParallelF.value(fa)) - fiberB <- F.start(ParallelF.value(fb.value)) - - // start a pair of supervisors to ensure that the opposite is canceled on error - _ <- F start { - fiberB.join flatMap { - case Outcome.Succeeded(_) => F.unit - case _ => fiberA.cancel - } - } - - _ <- F start { - fiberA.join flatMap { - case Outcome.Succeeded(_) => F.unit - case _ => fiberB.cancel - } - } - - a <- F - .onCancel(poll(fiberA.join), bothUnit(fiberA.cancel, fiberB.cancel)) - .flatMap[A] { - case Outcome.Succeeded(fa) => - fa - - case Outcome.Errored(e) => - fiberB.cancel *> F.raiseError(e) - - case Outcome.Canceled() => - fiberB.cancel *> poll { - fiberB.join flatMap { - case Outcome.Succeeded(_) | Outcome.Canceled() => - F.canceled *> F.never - case Outcome.Errored(e) => - F.raiseError(e) - } - } - } - - z <- F.onCancel(poll(fiberB.join), fiberB.cancel).flatMap[Z] { - case Outcome.Succeeded(fb) => - fb.map(b => f(a, b)) - - case Outcome.Errored(e) => - F.raiseError(e) - - case Outcome.Canceled() => - poll { - fiberA.join flatMap { - case Outcome.Succeeded(_) | Outcome.Canceled() => - F.canceled *> F.never - case Outcome.Errored(e) => - F.raiseError(e) - } - } - } - } yield z - } - ) - ) + f: (A, B) => Z): ParallelF[F, Z] = { + ParallelF(F.both(ParallelF.value(fa), ParallelF.value(fb)).map { case (a, b) => f(a, b) }) + } final override def ap[A, B](ff: ParallelF[F, A => B])( fa: ParallelF[F, A]): ParallelF[F, B] = @@ -194,10 +68,6 @@ trait GenSpawnInstances { final override def unit: ParallelF[F, Unit] = ParallelF(F.unit) - - // assumed to be uncancelable - private[this] def bothUnit(a: F[Unit], b: F[Unit]): F[Unit] = - F.start(a).flatMap(f => b *> f.join.void) } implicit def alignForParallelF[F[_], E](implicit F: GenSpawn[F, E]): Align[ParallelF[F, *]] = From cb8009932a0a8381208c7f5a051aa9396456ff00 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Mon, 20 Feb 2023 01:38:47 +0100 Subject: [PATCH 08/11] scalafmt --- .../scala/cats/effect/kernel/instances/GenSpawnInstances.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala b/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala index 8dafcb049b..f75c928243 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala @@ -51,7 +51,8 @@ trait GenSpawnInstances { final override def map2[A, B, Z](fa: ParallelF[F, A], fb: ParallelF[F, B])( f: (A, B) => Z): ParallelF[F, Z] = { - ParallelF(F.both(ParallelF.value(fa), ParallelF.value(fb)).map { case (a, b) => f(a, b) }) + ParallelF( + F.both(ParallelF.value(fa), ParallelF.value(fb)).map { case (a, b) => f(a, b) }) } final override def ap[A, B](ff: ParallelF[F, A => B])( From 2c4bc684daca18b77239194196dcabd58e40a95f Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Fri, 17 Feb 2023 17:59:36 +0100 Subject: [PATCH 09/11] New map2 implementation --- .../kernel/instances/GenSpawnInstances.scala | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala b/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala index f75c928243..cc2324c93d 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala @@ -18,7 +18,7 @@ package cats.effect.kernel.instances import cats.{~>, Align, Applicative, CommutativeApplicative, Functor, Monad, Parallel} import cats.data.Ior -import cats.effect.kernel.{GenSpawn, ParallelF} +import cats.effect.kernel.{GenSpawn, Outcome, ParallelF} import cats.implicits._ trait GenSpawnInstances { @@ -52,9 +52,42 @@ trait GenSpawnInstances { final override def map2[A, B, Z](fa: ParallelF[F, A], fb: ParallelF[F, B])( f: (A, B) => Z): ParallelF[F, Z] = { ParallelF( - F.both(ParallelF.value(fa), ParallelF.value(fb)).map { case (a, b) => f(a, b) }) + F.uncancelable { poll => + F.start(ParallelF.value(fa)).flatMap { fia => + F.start(F.guaranteeCase(ParallelF.value(fb)) { + case Outcome.Succeeded(_) => F.unit + case _ => fia.cancel + }).flatMap { fib => + F.onCancel(poll(fia.join), bothUnit(fia.cancel, fib.cancel)).flatMap { + case Outcome.Succeeded(fa) => + F.onCancel(poll(fib.join), fib.cancel).flatMap { + case Outcome.Succeeded(fb) => + F.map2(fa, fb)(f) + case Outcome.Errored(e) => + F.raiseError(e) + case Outcome.Canceled() => + poll(F.canceled *> F.never) + } + case Outcome.Errored(e) => + fib.cancel *> F.raiseError(e) + case Outcome.Canceled() => + fib.cancel *> poll(fib.join).flatMap { + case Outcome.Errored(e) => + F.raiseError(e) + case _ => + poll(F.canceled *> F.never) + } + } + } + } + } + ) } + // assumed to be uncancelable + private[this] final def bothUnit(a: F[Unit], b: F[Unit]): F[Unit] = + F.start(a).flatMap { fib => F.guarantee(b, fib.join.void) } + final override def ap[A, B](ff: ParallelF[F, A => B])( fa: ParallelF[F, A]): ParallelF[F, B] = map2(ff, fa)(_(_)) From 5ffdb35bb540e8bbd445dea59956c7a312a8c000 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Mon, 20 Feb 2023 01:34:21 +0100 Subject: [PATCH 10/11] Temporarily disable 2 PureConc tests (probably issue #3430) --- .../test/scala/cats/effect/laws/PureConcSpec.scala | 6 ++++-- .../shared/src/test/scala/cats/effect/IOSpec.scala | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala b/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala index cc2842873a..1e596d722d 100644 --- a/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala +++ b/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala @@ -43,10 +43,11 @@ class PureConcSpec extends Specification with Discipline with BaseSpec { type F[A] = PureConc[Int, A] val F = GenConcurrent[F] + // fails probably due to issue #3430 "short-circuit on error" in { pure.run((F.never[Unit], F.raiseError[Unit](42)).parTupled) mustEqual Outcome.Errored(42) pure.run((F.raiseError[Unit](42), F.never[Unit]).parTupled) mustEqual Outcome.Errored(42) - } + }.pendingUntilFixed "short-circuit on canceled" in { pure.run((F.never[Unit], F.canceled).parTupled.start.flatMap(_.join)) mustEqual Outcome @@ -55,6 +56,7 @@ class PureConcSpec extends Specification with Discipline with BaseSpec { .Succeeded(Some(Outcome.canceled[F, Nothing, Unit])) } + // fails probably due to issue #3430 "not run forever on chained product" in { import cats.effect.kernel.Par.ParallelF @@ -65,7 +67,7 @@ class PureConcSpec extends Specification with Discipline with BaseSpec { ParallelF.value( ParallelF(fa).product(ParallelF(fb)).product(ParallelF(fc)))) mustEqual Outcome .Errored(42) - } + }.pendingUntilFixed "ignore unmasking in finalizers" in { val fa = F.uncancelable { poll => F.onCancel(poll(F.unit), poll(F.unit)) } diff --git a/tests/shared/src/test/scala/cats/effect/IOSpec.scala b/tests/shared/src/test/scala/cats/effect/IOSpec.scala index 2b05e0a405..64f7391134 100644 --- a/tests/shared/src/test/scala/cats/effect/IOSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/IOSpec.scala @@ -1335,6 +1335,19 @@ class IOSpec extends BaseSpec with Discipline with IOPlatformSpecification { tsk must completeAs(42) } + + "not run forever on chained product" in ticked { implicit ticker => + import cats.effect.kernel.Par.ParallelF + + case object TestException extends RuntimeException + + val fa: IO[String] = IO.pure("a") + val fb: IO[String] = IO.pure("b") + val fc: IO[Unit] = IO.raiseError[Unit](TestException) + val tsk = + ParallelF.value(ParallelF(fa).product(ParallelF(fb)).product(ParallelF(fc))).void + tsk must failAs(TestException) + } } "miscellaneous" should { From 35c8c4333d87862f3321f9cbdf685447dad94c52 Mon Sep 17 00:00:00 2001 From: Daniel Urban Date: Wed, 19 Apr 2023 20:40:31 +0200 Subject: [PATCH 11/11] Go back to the old map2 implementation again --- .../kernel/instances/GenSpawnInstances.scala | 37 +------------------ .../scala/cats/effect/laws/PureConcSpec.scala | 6 +-- 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala b/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala index cc2324c93d..f75c928243 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/instances/GenSpawnInstances.scala @@ -18,7 +18,7 @@ package cats.effect.kernel.instances import cats.{~>, Align, Applicative, CommutativeApplicative, Functor, Monad, Parallel} import cats.data.Ior -import cats.effect.kernel.{GenSpawn, Outcome, ParallelF} +import cats.effect.kernel.{GenSpawn, ParallelF} import cats.implicits._ trait GenSpawnInstances { @@ -52,42 +52,9 @@ trait GenSpawnInstances { final override def map2[A, B, Z](fa: ParallelF[F, A], fb: ParallelF[F, B])( f: (A, B) => Z): ParallelF[F, Z] = { ParallelF( - F.uncancelable { poll => - F.start(ParallelF.value(fa)).flatMap { fia => - F.start(F.guaranteeCase(ParallelF.value(fb)) { - case Outcome.Succeeded(_) => F.unit - case _ => fia.cancel - }).flatMap { fib => - F.onCancel(poll(fia.join), bothUnit(fia.cancel, fib.cancel)).flatMap { - case Outcome.Succeeded(fa) => - F.onCancel(poll(fib.join), fib.cancel).flatMap { - case Outcome.Succeeded(fb) => - F.map2(fa, fb)(f) - case Outcome.Errored(e) => - F.raiseError(e) - case Outcome.Canceled() => - poll(F.canceled *> F.never) - } - case Outcome.Errored(e) => - fib.cancel *> F.raiseError(e) - case Outcome.Canceled() => - fib.cancel *> poll(fib.join).flatMap { - case Outcome.Errored(e) => - F.raiseError(e) - case _ => - poll(F.canceled *> F.never) - } - } - } - } - } - ) + F.both(ParallelF.value(fa), ParallelF.value(fb)).map { case (a, b) => f(a, b) }) } - // assumed to be uncancelable - private[this] final def bothUnit(a: F[Unit], b: F[Unit]): F[Unit] = - F.start(a).flatMap { fib => F.guarantee(b, fib.join.void) } - final override def ap[A, B](ff: ParallelF[F, A => B])( fa: ParallelF[F, A]): ParallelF[F, B] = map2(ff, fa)(_(_)) diff --git a/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala b/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala index 1e596d722d..cc2842873a 100644 --- a/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala +++ b/laws/shared/src/test/scala/cats/effect/laws/PureConcSpec.scala @@ -43,11 +43,10 @@ class PureConcSpec extends Specification with Discipline with BaseSpec { type F[A] = PureConc[Int, A] val F = GenConcurrent[F] - // fails probably due to issue #3430 "short-circuit on error" in { pure.run((F.never[Unit], F.raiseError[Unit](42)).parTupled) mustEqual Outcome.Errored(42) pure.run((F.raiseError[Unit](42), F.never[Unit]).parTupled) mustEqual Outcome.Errored(42) - }.pendingUntilFixed + } "short-circuit on canceled" in { pure.run((F.never[Unit], F.canceled).parTupled.start.flatMap(_.join)) mustEqual Outcome @@ -56,7 +55,6 @@ class PureConcSpec extends Specification with Discipline with BaseSpec { .Succeeded(Some(Outcome.canceled[F, Nothing, Unit])) } - // fails probably due to issue #3430 "not run forever on chained product" in { import cats.effect.kernel.Par.ParallelF @@ -67,7 +65,7 @@ class PureConcSpec extends Specification with Discipline with BaseSpec { ParallelF.value( ParallelF(fa).product(ParallelF(fb)).product(ParallelF(fc)))) mustEqual Outcome .Errored(42) - }.pendingUntilFixed + } "ignore unmasking in finalizers" in { val fa = F.uncancelable { poll => F.onCancel(poll(F.unit), poll(F.unit)) }