cats-effect icon indicating copy to clipboard operation
cats-effect copied to clipboard

Potential inconsistency between joinWithNever and docs

Open wunderk1nd-e opened this issue 1 year ago • 5 comments
trafficstars

Hi there,

In the docs for the the Spawn typeclass at the very end it states:

In English, the semantics of this are as follows:

If the child fiber completed successfully, produce its result If it errored, re-raise the error within the current fiber If it canceled, attempt to self-cancel, and if the self-cancelation fails, deadlock Sometimes this is an appropriate semantic, and the cautiously-verbose joinWithNever function implements it for you.

However, the joinWithNever implementation doesn't look like it actually makes any attempts at self cancellation and I think I've confirmed this with a small test app.

object FunTest extends IOApp.Simple {
  override val run: IO[Unit] = for {
    _ <- IO.println("Starting")
    child = IO.println("Starting child...") >>
      IO.sleep(1.second) >>
      IO.canceled >>
      IO.sleep(1.second) >>
      IO.println("Child completed")
    childFiber     <- child.start
    dependentChild <- childFiber.joinWithNever.as("I managed to return something").start
    result <- dependentChild
      .joinWith(IO.pure("I was cancelled"))
      .timeoutTo(3.seconds, "I waited too long :(".pure[IO])
    _ <- IO.println(s"result is $result")
  } yield ()
}

which outputs the following to console

Starting
Starting child...
result is I waited too long :(

Process finished with exit code 0

wunderk1nd-e avatar Jun 26 '24 16:06 wunderk1nd-e

Yeah, that's definitely a bug; either in the code or in the docs.

durban avatar Jun 28 '24 19:06 durban

I'm reminded of a similar confusion with embedNever which we discussed in https://github.com/typelevel/cats-effect/pull/3351#discussion_r1070651800.

armanbilge avatar Sep 08 '24 14:09 armanbilge

yeah, tbh I wasn't sure which way I should go fix the docs or fix the code. My intuition is joinWithNever seems safer as the doc says then the current implementation. I'm happy to fix the doc too if We want to keep the current semantic of joinWithNever.

lenguyenthanh avatar Sep 08 '24 18:09 lenguyenthanh

@lenguyenthanh your PR passes our test suite so that's a good sign that we are not depending on the bug and hopefully nobody else is either. Also we have precedent for fixing bugs even if they did cause some breakage ... (see the async_ bugfix in 3.5), not saying we want to repeat that necessarily, just saying we did do it 😅

armanbilge avatar Sep 09 '24 03:09 armanbilge

I agree that We need to be careful about the changes. So, the question is what does it take to introduce this change? Or should we be safe and update the docs instead?

lenguyenthanh avatar Sep 09 '24 08:09 lenguyenthanh

As we prefer updating the doc (#4131), how about update the docs like this (just copy/paste from scala doc)?:

-- If it canceled, attempt to self-cancel, and if the self-cancelation fails, **deadlock**
+- If it canceled, the caller is indefinitely suspended without termination (a.k.a **deadlock**)

lenguyenthanh avatar Oct 26 '24 19:10 lenguyenthanh

Was fixed by #4160.

durban avatar Nov 29 '24 20:11 durban