cats-effect
cats-effect copied to clipboard
Resource#race: late looser release
Consider a snippet:
import cats.syntax.all._
import cats.effect._
import cats.effect.std._
import scala.concurrent.duration._
object ResourceAllocRace extends IOApp.Simple {
private val con = Console[IO]
private def mkResource(name: String, random: Random[IO]): Resource[IO, String] =
Resource.make {
con.println(s"... [$name]: connecting") >>
random.nextIntBounded(3000).flatMap(ms => IO.sleep(ms.millis)) >>
con.println(s">>> [$name]: CONNECTED")
}(_ => con.println(s"<<< [$name]: CLOSED"))
.as(name)
override def run: IO[Unit] =
Random.scalaUtilRandom[IO].flatMap { random =>
(
((
mkResource("AAA", random) race mkResource("BBB", random)
) race (
mkResource("CCC", random) race mkResource("DDD", random)
)) race ((
mkResource("EEE", random) race mkResource("FFF", random)
) race (
mkResource("GGG", random) race mkResource("HHH", random)
))
).map(_.merge.merge.merge).flatMap { winner =>
Resource.eval(con.println(s"!!! [$winner]: WON"))
}.use { _ =>
con.println("... working") >>
IO.sleep(10.seconds) >> // can be increased to any value
con.println("... DONE")
}
}
}
When I run it, I may get the following output:
[info] running (fork) ResourceAllocRace
[info] ... [CCC]: connecting
[info] ... [AAA]: connecting
[info] ... [EEE]: connecting
[info] ... [BBB]: connecting
[info] ... [DDD]: connecting
[info] ... [FFF]: connecting
[info] ... [GGG]: connecting
[info] ... [HHH]: connecting
[info] >>> [HHH]: CONNECTED
[info] >>> [EEE]: CONNECTED
[info] >>> [AAA]: CONNECTED
[info] >>> [FFF]: CONNECTED
[info] <<< [FFF]: CLOSED
[info] >>> [CCC]: CONNECTED
[info] >>> [DDD]: CONNECTED
[info] <<< [DDD]: CLOSED
[info] >>> [BBB]: CONNECTED
[info] <<< [BBB]: CLOSED
[info] >>> [GGG]: CONNECTED
[info] <<< [GGG]: CLOSED
[info] !!! [CCC]: WON
[info] ... working
[info] ... DONE
[info] <<< [HHH]: CLOSED
[info] <<< [EEE]: CLOSED
[info] <<< [CCC]: CLOSED
[info] <<< [AAA]: CLOSED
-
Expected behavior:
only a winner resource is kept until the end of the
use
block. -
Actual behavior:
some of looser resources may be kept (not every time though but quite often) until the end of the
use
block along with the winner one. - Build info:
- Scala: 2.13.8
- Cats Effect: 3.3.14
- System props:
os.arch: x86_64 os.name: Mac OS X os.version: 12.4 java.vm.name: OpenJDK 64-Bit Server VM java.vm.vendor: Homebrew java.vm.version: 11.0.15+0
This surprises me immensely. I have the same expectation as you do.
I think this is related to this comment on Resource#start
:
* 4. If the fiber succeeds and .cancel lost the race or wasn't called,
* finalize naturally when the containing scope ends, `join` returns
* the value
It seems that .cancel
can indeed lose the race (and it does for H, E and A in the example), and so they are not finalized eagerly. Unless I'm misunderstanding something, the "finalize naturally when the containing scope ends" part means exactly what is observed in the example. (I have no idea what is the reason for this behavior, but it's documented, so there must be a reason...)
That comment seems to have been added/updated in:
- https://github.com/typelevel/cats-effect/pull/1733
After staring at this a bit I have a feeling it's like https://github.com/typelevel/cats-effect/issues/1620#issuecomment-1177947475. I don't think there's anything fundamentally wrong going on here per se, this is just another example of a method which has more than one lawful implementation and that we'll need to override the default implementation to get the particular semantics we want.