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

unsafeToFutureCancelable's cancel future completes before setting the result future's cancellation status #4047

Open
mperucca opened this issue Mar 27, 2024 · 7 comments

Comments

@mperucca
Copy link

The following fails fairly quickly:

while (true) {
  val (future, cancel) = IO.never.unsafeToFutureCancelable()
  Await.result(cancel(), Duration.Inf)
  assert(future.isCompleted)
}

I believe future actually has finished it's work, as the following doesn't fail:

while (true) {
  @volatile var started = false
  @volatile var done = false
  val io = IO { started = true }.bracket(_ => IO.never)(_ => IO { done = true })
  val (_, cancel) = io.unsafeToFutureCancelable()
  while (!started) {}
  Await.result(cancel(), Duration.Inf)
  assert(done)
}

This was mostly confusing in unit tests that were nondeterministic as they tested that the cancellation worked. I think it'd be more intuitive if the future's cancellation status was set before cancel completes, but maybe there's something else unintuitive that would result from that that I'm not considering.

@durban
Copy link
Contributor

durban commented Mar 30, 2024

We could "fix" this, but then we'd have the opposite problem: the original Future would be isCompleted strictly before the cancel Future. I wonder which is more intuitive? The "most correct" would probably be to have them completed at exactly the same time. Except that's not really a thing with Futures. In theory we could try to do something with implementing Future directly... but honestly, I don't think it's worth it. I think isCompleted is just inherently racy...

@mperucca
Copy link
Author

the original Future would be isCompleted strictly before the cancel Future

I assumed that's how it was intended to work, but perhaps it's less intuitive than I thought.

While pondering, I decided to try the following program that avoids Future altogether:

for {
  a <- IO.never.start
  winner <- a.join race a.cancel
} yield winner

Seems that sometimes the result is Left(Canceled()), sometimes it's Right(()), and sometimes it hangs. 🤔

@durban
Copy link
Contributor

durban commented Mar 31, 2024

Uhh, that definitely shouldn't hang. It's fine that the result is sometimes Left/Right (race is nondeterministic), but if it hangs, that's a bug.

@durban
Copy link
Contributor

durban commented Apr 5, 2024

@mperucca Could you provide the complete code that you see hanging? I tried, but couldn't get it to hang with the snippet above...

@mperucca
Copy link
Author

mperucca commented Apr 5, 2024

Well I've been trying that snippet again in a loop (along with println) in a couple different versions of cats effect and Scala, and I can only reproduce the occasional hang when running on Scastie where it gets partway through, so I'd say it's just internet problems there. I'm real sorry for the false alarm.

I still think the original issue is surprising behavior, but maybe that's just me.

@durban
Copy link
Contributor

durban commented Apr 6, 2024

Thanks for trying it again.

I was thinking about the original issue too, and I tend to agree, that the behavior you expected is the "more intuitive" one. Although, I'd like to see what others think...

@armanbilge
Copy link
Member

armanbilge commented Apr 22, 2024

I tend to agree, that the behavior you expected is the "more intuitive" one.

Me too. However, this is due to a race condition and is not easy to solve.

In theory we could try to do something with implementing Future directly... but honestly, I don't think it's worth it.

Can't say if it's worth it, but actually I think this is an interesting idea i.e. IOFiber[A] extends Future[A]. An IOFiber is a handle to a running task in the same way that a Future is. It would be pretty cool if we can "convert" to a Future simply by directly returning that handle. I started playing with this idea in 5b7bb92 but don't have more time now to chase it further, maybe someone would be interested to pick it up :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants