Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a safer Resource.attempt which releases acquired resource(s) in case of error #4128

Open
wants to merge 12 commits into
base: series/3.x
Choose a base branch
from

Conversation

lenguyenthanh
Copy link
Member

@lenguyenthanh lenguyenthanh commented Sep 2, 2024

This is my solution for #3757,

As I discussed with @armanbilge, the new method requires MonadCancelThrow in order to use Resource.allocatedCase which handles releasing acquired resource(s) in case of error. This pr also removed implicit ApplicativeError and deprecated the current attempt method.

This is a draft as it still needs to add tests and address some issues that I'll comment in the pr. But please give me early feedback.

Which releases acquired resource(s) in case of error and `MonadCancelThrow`
constrain is required in order to do that. This also take away implicit
`ApplicativeError` and deprecated the current attempt method.
@@ -1462,7 +1473,7 @@ 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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this still used the old attempt but We can't use the new one because the error type is not fixed to Throwable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should mark this instance as deprecated and create a new instance that requires MonadCancelThrow.

Copy link
Member Author

@lenguyenthanh lenguyenthanh Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in cb1a293. But now

private[effect] trait ResourceHOInstances4 extends ResourceHOInstances5 {
implicit def catsEffectMonadErrorForResource[F[_], E](
implicit F0: MonadError[F, E]): MonadError[Resource[F, *], E] =
new ResourceMonadError[F, E] {
def F = F0
}
}
complains because it use the deprecated ResourceMonadError.

I'm not sure what the best way to fix this. I feel like We can remove it completely. But maybe it broke client code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We deprecate this one too :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We deprecate this one but keep the trait hierarchy?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question. Maybe we should try to deprioritize this instance by moving it to the bottom of the hierarchy. Alternatively, we could just remove the implicit modifier.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept the hierachy the same but removed implicit modifier and deprecated catsEffectMonadErrorForResource function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it more and actually I think it's okay to leave the implicit there. We don't need to change any prioritization, because we aren't introducing any new instances, and the instance derived from MonadCancelThrow (which is required for the fixed attempt) is already higher priority than this one.

By leaving it implicit, users will get an actionable deprecation instead of non-compiling code.


def handleErrorWith[B >: A, E](f: E => Resource[F, B])(
implicit F: ApplicativeError[F, E]): Resource[F, B] =
attempt.flatMap {
attempt(F).flatMap {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo, We should add a "safe" variant of handleErrorWith as well to avoid inconsistent behaviors between this and attempt.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. You are right, we definitely should do that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 3c2d6af

@@ -1462,7 +1473,7 @@ 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should mark this instance as deprecated and create a new instance that requires MonadCancelThrow.


def handleErrorWith[B >: A, E](f: E => Resource[F, B])(
implicit F: ApplicativeError[F, E]): Resource[F, B] =
attempt.flatMap {
attempt(F).flatMap {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. You are right, we definitely should do that.

Comment on lines 704 to 705
def attempt[E](F: ApplicativeError[F, E]): Resource[F, Either[E, A]] = {
implicit val x: ApplicativeError[F, E] = F
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do a trick here to automatically fix more callsites without requirng them to recompile: we can pattern match on F and if it is Sync or Async then we can use the fixed implementation instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to implement your suggestion.

I tried:

  @deprecated("Use overload with MonadCancelThrow", "3.6.0")
  def attempt[E](F: ApplicativeError[F, E]): Resource[F, Either[E, A]] =
    F match {
      case x: Async[_] =>
        implicit val x: MonadCancelThrow[F] = x
        attempt
      case x: Sync[_] => attempt(x)
        implicit val x: MonadCancelThrow[F] = x
        attempt
      case _ => ???
   }

But it doesn't work as attempt with MonadCancelThrow returns Resource[F, Either[Throwable, A]] but not Resource[F, Either[E, A]]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can cast unsafely. If F is Sync then E must be Throwable. Also I just realized since Async <: Sync we only need to match for Sync.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed it 3b7d0ee


def attempt(implicit F: MonadCancelThrow[F]): Resource[F, Either[Throwable, A]] =
Resource.applyCase[F, Either[Throwable, A]] {
allocatedCase.attempt.map {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: shouldn't the allocation itself be interruptible?

like Resource.applyFull { poll => poll(allocatedCase).attempt.map...

Copy link
Member

@armanbilge armanbilge Sep 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, this looks right. Let's make sure to add a test case for this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though about using poll but didn't know for sure. I added tests and fix this in 07fa387 and 2839446

@@ -66,7 +66,7 @@ private[effect] object JvmCpuStarvationMetrics {
case (mbeanServer, _) => IO.blocking(mbeanServer.unregisterMBean(mBeanObjectName))
}
.map(_._2)
.handleErrorWith[CpuStarvationMetrics, Throwable] { th =>
.handleErrorWith[CpuStarvationMetrics] { th =>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change because handleErrorWith[E, A] now requires explicit ApplicativeError

Fix F is `Sync`, We know that it has to be a `MonadCancelThrow`,
therefore We can use safer attempt and `unsafely` cast the result.
Also remove implicit modifier from it.
.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 { _ => ref.get.map { _ must be_==(2) } }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of ignoring the value _ we can check that it's a Left.

tests/shared/src/test/scala/cats/effect/ResourceSpec.scala Outdated Show resolved Hide resolved
lenguyenthanh and others added 4 commits September 8, 2024 15:00
MonadCancelThrow instance (which is required for the fixed attempt)
is already higher priority than this one. By leaving it implicit,
users will get an actionable deprecation instead of non-compiling code.
Copy link
Member

@armanbilge armanbilge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, one last comment :)

Co-authored-by: Arman Bilge <[email protected]>
Copy link
Member

@armanbilge armanbilge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work :)

@armanbilge armanbilge added this to the v3.6.0 milestone Sep 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants