-
Couldn't load subscription status.
- Fork 1.4k
WIP cats-effect Concurrent instance #267
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
WIP cats-effect Concurrent instance #267
Conversation
|
Hi @Kaishh,
It's not a fire and forget. I changed both
That said If that's not expected behavior for ZIO, I'd like to know why ... what is your |
|
@alexandru It waits for the target fiber to die completely. That being said, wouldn't |
|
As a general opinion, That said, in this case the behavior of our |
|
For now I would implement I'll open a ticket on the Cats-Effect side and maybe fix that law for the next release. |
|
@Kaishh also, thanks for doing this. Type class laws are hard to come up with because there's a natural tendency to introduce assumptions based on how the test implementation behaves. And this one slipped through. |
|
@Kaishh could the next 2 issues be due to the same issue? Both of them are using |
|
@alexandru The Haskell community lived with a You can read more here. Btw would you be interested in making the Monix Task implement the Haskell / ZIO semantic? Ordinary Cats IO could stay the way it is if the behavior on blocking is omitted from the laws. |
3351dbf to
2bf9060
Compare
The later ones are discovered after changing Fiber.cancel, I think there canceller is not being run for some reason, but it's not obvious for me to pinpoint yet, since equivalent implementations with ZIO work |
|
I've also changed ContextShift implementation from |
Nice. 👍
Not waiting for a fiber to terminate before returning could potentially introduce a race condition. If you need help with this one I can try to take a closer look. |
|
@jdegoes Feel free to if you have the time! I'll have more time closer to the end of the week |
|
@Kaishh for testing purposes I just published a hash version with the laws being fixed, see PR typelevel/cats-effect#376: Please test with it and see if the laws are still failing. |
9669fb6 to
cc6725d
Compare
|
@alexandru Still diverging at 'asyncF registration can be canceled' now without .fork.void |
|
If you can add this to ZIO in a simplified form with ZIO data types ( def asyncFRegisterCanBeCancelled[A](a: A) = {
val lh = for {
release <- Deferred[F, A]
acquire <- Deferred[F, Unit]
task = F.asyncF[Unit] { _ =>
F.bracket(acquire.complete(()))(_ => F.never[Unit])(_ => release.complete(a))
}
fiber <- F.start(task)
_ <- acquire.get
_ <- fiber.cancel
a <- release.get
} yield a
lh <-> F.pure(a)
}And it fails, then we will investigate and fix ASAP. The test looks fine to me. |
|
@jdegoes The IO version of this test is there since the beginning – https://github.com/scalaz/scalaz-zio/pull/267/files#diff-de7c402961556a213ccd2cf8962f18afR515 The problem is that it works |
|
Now that I think about it, there's a paradigm mismatch between ZIO and ConcurrentEffect/ContextShift. cats IO's ConcurrentEffect/ContextShift are parameterised by an execution context, as long as you use a pair created from the same @alexandru Should this be a part of ConcurrentEffect/ContextShift laws? Libraries are already making this assumption in their API's, e.g. fs2: http4s: |
|
ContextShift.ExecuteOn happened due to the need to execute a certain task on a very specific thread pool. Such tasks involve blocking I/O. But when you do that, you don’t want to stay on the thread pool meant for blocking I/O for the bind continuation of that task. You want to shift back to some thread pool that you’re using for CPU-bound stuff. This is a common pattern on the JVM that happens naturally with, say, Future or whatever Java does these days. It’s a problem created by the platform’s ability to block 1:1 threads. I did not want |
|
@alexandru What happens in monix if the task inside |
With But yes, what you're describing is what eventually happens with solutions based on A solution based on |
|
@alexandru Ok, do you think it's reasonable to add a law or spec to ContextShift that makes it mandatory for |
|
@Kaishh currently The reason is that it is difficult to describe such laws. We have a Plus implementations (like the current ZIO apparently) cannot describe But I guess we should at least make it clear in the documentation or something like that. |
|
Well, now that I think about it, a solution based on our Maybe we'll make it happen. Unfortunately I'm all caught up in Monix development atm, so it's low priority.l |
This is true but only by changing the yield rate, which defaults to what is essentially "no yielding". I feel there's a contradiction between |
I think that at this point you're bringing your Fiber semantics into the picture unnecessarily. When blocking I/O is involved, users want to execute that blocking I/O on a separate thread-pool, because blocking I/O is a rare operation that also needs protection against threads starvation and then jump to the thread pool for CPU-bound operations afterwards. You don't want 3 or more thread-pools, you want just 2, at most — that it happens in practice for projects to work with 3 or more, that's only because of opinionated and poorly defined libraries. If there is such a thing as the IO's "execution context", then users should be able to take advantage of it. Cats-Effect's IO does not have such an execution context, which is fine, but then its run-loop doesn't auto-fork. |
cc6725d to
1458906
Compare
|
@Kaishh @jdegoes I investigated this a little bit and found out that the ZIO version of Currently: def testAsyncPureCreationIsInterruptible = {
val io = for {
release <- Promise.make[Nothing, Int]
acquire <- Promise.make[Nothing, Unit]
task = IO.asyncPure[Nothing, Unit] { _ =>
IO.bracket(acquire.complete(()))(_ => IO.never)(_ => release.complete(42).void)
}
fiber <- task.fork
_ <- acquire.get
_ <- fiber.interrupt
a <- release.get
} yield a
unsafeRun(io) must_=== 42
}The bracket arguments are in wrong order: Hope this helps! (with FS2 1.0 and http4s 0.19 both requiring ConcurrentEffect in various places, this ticket is much needed for interop). |
88c627e to
3a3c160
Compare
|
@jdegoes travis passes now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
YAY 🎉 This is really awesome.
interop-cats-laws/src/test/scala/scalaz/zio/interop/catzSpec.scala
Outdated
Show resolved
Hide resolved
| private class CatsEffect extends CatsMonadError[Throwable] with Effect[Task] with CatsSemigroupK[Throwable] with RTS { | ||
| protected def exitResultToEither[A]: ExitResult[Throwable, A] => Either[Throwable, A] = _.toEither | ||
| @inline final protected def exitResultToEither[A](e: ExitResult[Throwable, A]): Either[Throwable, A] = | ||
| e.fold(_.checked[Throwable] match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense. Another option would be if there's no checked errors, peeling off the first unchecked error. Not sure it matters much, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Matters for existing code that's catching a specific throwable (e.g. doobie). It's hard to decide what would be best here, as on one hand existing code would have no way of dealing with synthetic exceptions from combined Fibers, but OTOH we want to preserve as much information about errors as possible.
| release: A => Task[Unit] | ||
| ): Task[B] = IO.bracket(acquire)(release(_).catchAll(IO.terminate(_)))(use) | ||
| ): Task[B] = | ||
| IO.bracket(acquire)(release(_).catchAll(IO.terminate))(use) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
io.catchAll(IO.terminate) is so common we could do a method to implement that pattern: io.orDie: IO[Nothing, A]. This way we could make it zero cost. I'll open a ticket if you like the idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea! I've had multiple questions wrt 'how do I make IO[Nothing, ?] from IO[Throwable, ?]'. I think the name might benefit from being long and explicit, e.g. io.dieOnThrowable
|
@Kaishh Superb work on this! Thanks for all your help and your patience in getting this in. |
|
@regiskuckaertz I moved out raceAttempt #409 to remove unnecessary impediments to merging |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ready to merge on this if @regiskuckaertz doesn't have any objections! There are a couple conflicts, looks like, but easy to fix.
|
🎸 🕺 |
|
@Kaishh Superb work! And amazing patience. 😆 Thanks for all your work on this one... |
|
👍 nice |
| } | ||
| } | ||
|
|
||
| (1 to 50).foreach { s => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to run those 50 times anymore, do we?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully we don't. But, these tests are cheap enough to run and each iteration gives a slightly higher chance of spotting a regression. I know this is a defensive code smell, but IMHO justified by the domain - these are effectively integration tests, that also highly depend on JVM behavior and on upstream cats-effect.
There are several issues
Fiber.cancelimplementation is changed fromfiber.interrupttofiber.interrupt.fork.voidmvar.putblocks and is uninterruptible, so waiting on it can't work. @alexandru I guesscanceldocs should mention that it should be 'fire-and-forget'.