diff --git a/core/jvm/src/main/scala/cats/effect/metrics/JvmCpuStarvationMetrics.scala b/core/jvm/src/main/scala/cats/effect/metrics/JvmCpuStarvationMetrics.scala index 9a8dc6e6c7..d52a20aa3f 100644 --- a/core/jvm/src/main/scala/cats/effect/metrics/JvmCpuStarvationMetrics.scala +++ b/core/jvm/src/main/scala/cats/effect/metrics/JvmCpuStarvationMetrics.scala @@ -66,7 +66,7 @@ private[effect] object JvmCpuStarvationMetrics { case (mbeanServer, _) => IO.blocking(mbeanServer.unregisterMBean(mBeanObjectName)) } .map(_._2) - .handleErrorWith[CpuStarvationMetrics, Throwable] { th => + .handleErrorWith[CpuStarvationMetrics] { th => Resource.eval(Console[IO].errorln(warning(th))).map(_ => new NoOpCpuStarvationMetrics) } } 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 6ee94c2dbc..94a9ebe384 100644 --- a/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala +++ b/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala @@ -700,28 +700,51 @@ sealed abstract class Resource[F[_], +A] extends Serializable { } } - def attempt[E](implicit F: ApplicativeError[F, E]): Resource[F, Either[E, A]] = - this match { - case Allocate(resource) => - Resource.applyFull { poll => - resource(poll).attempt.map { - case Left(error) => (Left(error), (_: ExitCase) => F.unit) - case Right((a, release)) => (Right(a), release) - } - } - case Bind(source, f) => - Resource.unit.flatMap(_ => source.attempt).flatMap { - case Left(error) => Resource.pure(error.asLeft) - case Right(s) => f(s).attempt + @deprecated("Use overload with MonadCancelThrow", "3.6.0") + def attempt[E](F: ApplicativeError[F, E]): Resource[F, Either[E, A]] = + F match { + case x: Sync[F] => + attempt(x).asInstanceOf[Resource[F, Either[E, A]]] + case _ => + implicit val x: ApplicativeError[F, E] = F + this match { + case Allocate(resource) => + Resource.applyFull { poll => + resource(poll).attempt.map { + case Left(error) => (Left(error), (_: ExitCase) => F.unit) + case Right((a, release)) => (Right(a), release) + } + } + case Bind(source, f) => + Resource.unit.flatMap(_ => source.attempt(F)).flatMap { + case Left(error) => Resource.pure(error.asLeft) + case Right(s) => f(s).attempt(F) + } + case p @ Pure(_) => + Resource.pure(p.a.asRight) + case e @ Eval(_) => + Resource.eval(e.fa.attempt) } - case p @ Pure(_) => - Resource.pure(p.a.asRight) - case e @ Eval(_) => - Resource.eval(e.fa.attempt) } + def attempt(implicit F: MonadCancelThrow[F]): Resource[F, Either[Throwable, A]] = + Resource.applyFull[F, Either[Throwable, A]] { poll => + poll(allocatedCase).attempt.map { + case Right((a, r)) => (a.asRight[Throwable], r) + case error => (error.asInstanceOf[Either[Throwable, A]], _ => F.unit) + } + } + + @deprecated("Use overload with MonadCancelThrow", "3.6.0") def handleErrorWith[B >: A, E](f: E => Resource[F, B])( - implicit F: ApplicativeError[F, E]): Resource[F, B] = + F: ApplicativeError[F, E]): Resource[F, B] = + attempt(F).flatMap { + case Right(a) => Resource.pure(a) + case Left(e) => f(e) + } + + def handleErrorWith[B >: A](f: Throwable => Resource[F, B])( + implicit F: MonadCancelThrow[F]): Resource[F, B] = attempt.flatMap { case Right(a) => Resource.pure(a) case Left(e) => f(e) @@ -1279,6 +1302,9 @@ private[effect] trait ResourceHOInstances3 extends ResourceHOInstances4 { } private[effect] trait ResourceHOInstances4 extends ResourceHOInstances5 { + @deprecated( + "Bring an implicit MonadCancelThrow[F] into scope to get the fixed Resource instance", + "3.6.0") implicit def catsEffectMonadErrorForResource[F[_], E]( implicit F0: MonadError[F, E]): MonadError[Resource[F, *], E] = new ResourceMonadError[F, E] { @@ -1330,10 +1356,19 @@ abstract private[effect] class ResourceFOInstances1 { } abstract private[effect] class ResourceMonadCancel[F[_]] - extends ResourceMonadError[F, Throwable] + extends ResourceMonad[F] with MonadCancel[Resource[F, *], Throwable] { implicit protected def F: MonadCancel[F, Throwable] + override def attempt[A](fa: Resource[F, A]): Resource[F, Either[Throwable, A]] = + fa.attempt + + def handleErrorWith[A](fa: Resource[F, A])(f: Throwable => Resource[F, A]): Resource[F, A] = + fa.handleErrorWith(f) + + def raiseError[A](e: Throwable): Resource[F, A] = + Resource.raiseError[F, A, Throwable](e) + def canceled: Resource[F, Unit] = Resource.canceled def forceR[A, B](fa: Resource[F, A])(fb: Resource[F, B]): Resource[F, B] = @@ -1455,6 +1490,7 @@ abstract private[effect] class ResourceAsync[F[_]] Resource.executionContext } +@deprecated("Use ResourceMonadCancel", "3.6.0") abstract private[effect] class ResourceMonadError[F[_], E] extends ResourceMonad[F] with MonadError[Resource[F, *], E] { @@ -1462,10 +1498,10 @@ abstract private[effect] class ResourceMonadError[F[_], E] implicit protected def F: MonadError[F, E] override def attempt[A](fa: Resource[F, A]): Resource[F, Either[E, A]] = - fa.attempt + fa.attempt(F) def handleErrorWith[A](fa: Resource[F, A])(f: E => Resource[F, A]): Resource[F, A] = - fa.handleErrorWith(f) + fa.handleErrorWith(f)(F) def raiseError[A](e: E): Resource[F, A] = Resource.raiseError[F, A, E](e) diff --git a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala index b917818531..a445393193 100644 --- a/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala +++ b/tests/shared/src/test/scala/cats/effect/ResourceSpec.scala @@ -1172,6 +1172,31 @@ class ResourceSpec extends BaseSpec with ScalaCheck with Discipline { } } + "attempt" >> { + + "releases resource on error" in ticked { implicit ticker => + IO.ref(0) + .flatMap { ref => + val resource = Resource.make(ref.update(_ + 1))(_ => ref.update(_ + 1)) + val error = Resource.raiseError[IO, Unit, Throwable](new Exception) + (resource *> error).attempt.use { r => + IO(r must beLeft) *> ref.get.map { _ must be_==(2) } + } + } + .void must completeAs(()) + } + + "acquire is interruptible" in ticked { implicit ticker => + val sleep = IO.never + val timeout = 500.millis + IO.ref(false).flatMap { ref => + val r = Resource.makeFull[IO, Unit] { poll => poll(sleep).onCancel(ref.set(true)) }(_ => + IO.unit) + r.attempt.timeout(timeout).attempt.use_ *> ref.get + } must completeAs(true) + } + } + "uncancelable" >> { "does not suppress errors within use" in real { case object TestException extends RuntimeException