-
Notifications
You must be signed in to change notification settings - Fork 534
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
Resource#attempt
holds onto acquired resources even in case of error
#3757
Comments
Hm, if it failed, why is the body of |
Yes you are exactly right. However although the resource acquisition failed (prior to the attempt) it has not run all the finalizers yet. |
So, you're basically saying, that since |
why does that feel strange? I'm drawing a distinction between |
Yes, exactly: it seems strange to me that those two would be different. (To be clear, we're obviously assuming |
👍
Hmm, yes, I think I interpret that law the same way. Thanks for the pointer, let me think about this a bit 🤔 |
Since I haven't mentioned it yet, the problem with the current semantics is that if you try to implement some sort of retry then failed resources don't get released in a timely fashion. For example, here's a rough sketch of a connection pool as used in Ember client or Skunk. def getSocketFromPool: Resource[IO, Socket[IO]] = ???
def raiseIfConnectionIsDead(socket: Socket[IO]): IO[Unit] = ???
def getAliveSocketFromPool =
getSocketFromPool.evalTap(raiseIfConnectionIsDead).attempt.flatMap {
case Right(socket) => Resource.pure(socket)
case Left(DeadConnection) => getAliveSocketFromPool // retry to obtain a fresh connection
case Left(ex) => Resource.raiseError(ex) // raise all other errors
} Under the current semantics, every time A similar bug plagued us in http4s/http4s#7216 (comment), although note |
Format is ugly, but this is relevant context: https://discord.com/channels/632277896739946517/839263556754472990/1141812346877132860
|
Just playing around, seems this law is not held for other transformer implementations e.g. //> using dep org.typelevel::cats-core::2.10.0
//> using option -Ykind-projector:underscores
import cats.*
import cats.data.*
import cats.syntax.all.*
@main def main =
val write = WriterT.tell[Option, String]("hello")
val error = (()).raiseError[WriterT[Option, String, _], Unit]
println((write *> error).attempt.run)
println((write *> error.attempt).run) Some((,Left(())))
Some((hello,Left(()))) |
I'm curious, what is the conclusion of this? Following the discussion here, it is lawful and even painful but nothing We can do. But in http4s/http4s#7445, @armanbilge said We should fix it for Edit: fixed typo |
@lenguyenthanh thanks for following up. @djspiewak and I discussed this more at some point and I think that Daniel came around and supports changing the behavior to fix this.
When we talk about lawfulness, we need some notion of equivalence between two effectual computations. If I remember correctly, what Daniel and I discussed was how to consider This would not be the first time that we adjusted when |
thanks @armanbilge for the explanation! I'll try to see what I can do. |
I've been looking at the code, but up until now I couldn't find a solution for this. Look at this code: cats-effect/kernel/shared/src/main/scala/cats/effect/kernel/Resource.scala Lines 705 to 711 in 7168625
To fix this issue, We need to execute the I probably missing something here, I don't know how to proceed. Any pointer is much appreciated. |
@lenguyenthanh that's all exactly right actually 💯
You can go ahead and add the |
@armanbilge here is my naive implementation, couldn't make it work with just // Can't use `attempt` function here as it would cause ambiguity with the other `attempt`
def safeAttempt(implicit F: Concurrent[F]): Resource[F, Either[Throwable, A]] = {
// We need a Ref[Option[Resource.ExitCase => F[Unit]]] because We want to release
// all on hold resources in case of error
Resource.eval(F.ref(none[ExitCase => F[Unit]]).flatMap { release =>
this
.allocatedCase
.flatMap { case (a, r) => release.update(_ => r.some).as(a) }
.attempt
.flatMap {
case Left(error) =>
release.get.flatMap(_.traverse_(_(ExitCase.Errored(error)))).as(error.asLeft[A])
case Right(a) => a.asRight[Throwable].pure[F]
}
})
} |
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.
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.
The text was updated successfully, but these errors were encountered: