Added Blocker, a new type for blocking execution contexts #556
Conversation
Codecov Report
@@ Coverage Diff @@
## master #556 +/- ##
=========================================
Coverage ? 88.79%
=========================================
Files ? 75
Lines ? 2124
Branches ? 131
=========================================
Hits ? 1886
Misses ? 238
Partials ? 0 |
|
Looks great. |
|
Any opinions on an alternative constructor that uses a semaphore to guard
calls to `eval`? Like `BoundedLinebacker`
…On Sat, Jun 8, 2019 at 9:18 AM Fabio Labella ***@***.***> wrote:
*@SystemFw* approved this pull request.
Looks great.
The only thing I would add is maybe a line to the scaladoc to somehow
convey that you need to have application level bounds since this is backed
by a cached threadpool. Even just a link to Daniel's gist would do.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#556?email_source=notifications&email_token=AAA42PWCBUPNMDQADKRLLNTPZOWRNA5CNFSM4HWHF7J2YY3PNVWWK3TUL52HS4DFWFIHK3DMKJSXC5LFON2FEZLWNFSXPKTDN5WW2ZLOORPWSZGOB27IOUY#pullrequestreview-247367507>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAA42PRJHFRX46XEI6VUB4TPZOWRNANCNFSM4HWHF7JQ>
.
|
|
With |
|
But |
| def fromExecutorService[F[_]](makeExecutorService: F[ExecutorService])( | ||
| implicit F: Sync[F]): Resource[F, Blocker] = | ||
| Resource | ||
| .make(makeExecutorService)(ec => F.delay { ec.shutdownNow(); () }) |
rossabaker
Jun 9, 2019
Member
Note that this is a graceless shutdown. It may be useful to have a version that takes a timeout and awaits termination until the timeout. And since there are various things we might want to do with the returned list of runnables, I think unsafeFromExecutionContext carries its weight.
Note that this is a graceless shutdown. It may be useful to have a version that takes a timeout and awaits termination until the timeout. And since there are various things we might want to do with the returned list of runnables, I think unsafeFromExecutionContext carries its weight.
mpilquist
Jun 9, 2019
Author
Member
Yeah, that was intentional in this case. The pool should be empty at the point of finalization or it's arguably a bug in the caller's code. Also, we don't have a safe way to shutdown with a timeout unless we were to spawn a thread dedicated to shutdown.
Yeah, that was intentional in this case. The pool should be empty at the point of finalization or it's arguably a bug in the caller's code. Also, we don't have a safe way to shutdown with a timeout unless we were to spawn a thread dedicated to shutdown.
rossabaker
Jun 9, 2019
Member
I'm squeamish about the discard of the return value of shutdownNow. I'm thinking a non-empty list of unrrun runnables should maybe be raised into an error.
Shutdown with a timeout can be done without a new thread by the awaitTermination call, which would, uh, block off the Blocker. It's true that there's no async listener. I'm coming around to it as a bad idea.
I'm squeamish about the discard of the return value of shutdownNow. I'm thinking a non-empty list of unrrun runnables should maybe be raised into an error.
Shutdown with a timeout can be done without a new thread by the awaitTermination call, which would, uh, block off the Blocker. It's true that there's no async listener. I'm coming around to it as a bad idea.
mpilquist
Jun 9, 2019
Author
Member
Right, awaitTermination would block a thread in the main CPU bound thread pool, which is problematic. Raising an error if task list is non-empty is probably a good idea. Think that should be controllable via a flag or just always the behavior?
Right, awaitTermination would block a thread in the main CPU bound thread pool, which is problematic. Raising an error if task list is non-empty is probably a good idea. Think that should be controllable via a flag or just always the behavior?
rossabaker
Jun 9, 2019
Member
I think always. It's easily recoverable, but users can't do anything about it unless we surface it. Something that extends IllegalStateException with a NonEmptyList of Runnables feels right to me.
I think always. It's easily recoverable, but users can't do anything about it unless we surface it. Something that extends IllegalStateException with a NonEmptyList of Runnables feels right to me.
|
Here's what FS2 looks like if using |
I like that. As a bonus, places where you genuinely need an |
|
Any opinions on the names of |
|
I think |
| * | ||
| * Instances of this class should *not* be passed implicitly. | ||
| */ | ||
| final class Blocker private (val context: ExecutionContext) { |
djspiewak
Jun 9, 2019
Member
This, however, might merit the word "unsafe". Perhaps unsafeContext? The point being that you want to discourage people from unwrapping it directly unless they really know what they're doing.
This, however, might merit the word "unsafe". Perhaps unsafeContext? The point being that you want to discourage people from unwrapping it directly unless they really know what they're doing.
mpilquist
Jun 9, 2019
Author
Member
How about blockingContext? I want to avoid making it difficult to integrate with APIs that take a plain ExecutionContext with an API note saying it should support blocking tasks (e.g. our very own ContextShift).
How about blockingContext? I want to avoid making it difficult to integrate with APIs that take a plain ExecutionContext with an API note saying it should support blocking tasks (e.g. our very own ContextShift).
djspiewak
Jun 10, 2019
Member
That's fair. Do you think we should also add an extra function to ContextShift which takes a Blocker and deprecate the ExecutionContext one? It feels weird to have two not-well-integrated parts of the API.
That's fair. Do you think we should also add an extra function to ContextShift which takes a Blocker and deprecate the ExecutionContext one? It feels weird to have two not-well-integrated parts of the API.
mpilquist
Jun 10, 2019
Author
Member
I thought so yeah, but then didn't do it to avoid breaking compatibility. But since this is a 2.0.0 release...
I thought so yeah, but then didn't do it to avoid breaking compatibility. But since this is a 2.0.0 release...
mpilquist
Jun 10, 2019
Author
Member
OK pushed a change -- I did NOT deprecate the EC variant of evalOn. See the ScalaDoc as to why.
OK pushed a change -- I did NOT deprecate the EC variant of evalOn. See the ScalaDoc as to why.
| /** | ||
| * Evaluates the supplied task on the blocking execution context via `evalOn`. | ||
| */ | ||
| def eval[F[_], A](fa: F[A])(implicit cs: ContextShift[F]): F[A] = |
djspiewak
Jun 9, 2019
Member
I kinda prefer the name evalOn, just to avoid having eval and evalOn which both do the same thing.
I kinda prefer the name evalOn, just to avoid having eval and evalOn which both do the same thing.
| NonEmptyList.fromList(tasks) | ||
| } | ||
| F.flatMap(tasks) { | ||
| case Some(t) => F.raiseError(new OutstandingTasksAtShutdown(t)) |
djspiewak
Jun 9, 2019
Member
We honestly might want to make the threads daemon, because there's a rather ugly situation that can arise here where someone has a stuck thread, the Resource scope closes, this error is thrown, the main thread halts, and then the program… hangs.
We honestly might want to make the threads daemon, because there's a rather ugly situation that can arise here where someone has a stuck thread, the Resource scope closes, this error is thrown, the main thread halts, and then the program… hangs.
| * This must not be used with general purpose contexts like | ||
| * `scala.concurrent.ExecutionContext.Implicits.global'. | ||
| */ | ||
| def wrapExecutionContext(ec: ExecutionContext): Blocker = new Blocker(ec) |
djspiewak
Jun 9, 2019
Member
wrap is also fine. :-)
wrap is also fine. :-)
…ext to blockingContext, and renamed wrap methods to lift
|
All review comments are addressed -- let me know if I missed anything. |
| val tasks = F.delay { | ||
| val tasks = ec.shutdownNow() | ||
| val b = List.newBuilder[Runnable] | ||
| val itr = tasks.iterator |
mpilquist
Jun 9, 2019
Author
Member
JavaConverters is deprecated in 2.13 and I don't want to complicate the build with 2.12/2.13 source folders over 1 conversion from a Java to Scala list.
JavaConverters is deprecated in 2.13 and I don't want to complicate the build with 2.12/2.13 source folders over 1 conversion from a Java to Scala list.
|
Overall, |
|
@jdegoes It contaminates the Scala.js API and steps in to effect specific territory no more so than |
|
The point of cats(-effect) is a set of abstractions that others can share and buy-in to if they would like to. 3rd party effect types are free to engage in discussion, but the progress of the library is independent of any implementations. |
|
One bikeshed of the exception below, but looks good. |
| def liftExecutionContext(ec: ExecutionContext): Blocker = new Blocker(ec) | ||
|
|
||
| /** Thrown if there are tasks queued in the thread pool at the time a `Blocker` is finalized. */ | ||
| final class OutstandingTasksAtShutdown(val tasks: NonEmptyList[Runnable]) extends IllegalStateException("There were outstanding tasks at time of shutdown of the thread pool") |
rossabaker
Jun 10, 2019
Member
[GitHub ate my review, retrying]
I like raising an error when Blocker closes with outstanding tasks, but should we let the global compute pool in IOApp exit successfully in the same scenario? A difference is that this is recoverable and that's not. I don't know what we would do with the NEL in IOApp other than exit non-zero and maybe log the (probably uselessly toStringed) runnables.
I bring it up here, wondering whether this should be nested or has other uses. But even if we change the behavior of IOApp, it's hard to see that the exception is useful there.
[GitHub ate my review, retrying]
I like raising an error when Blocker closes with outstanding tasks, but should we let the global compute pool in IOApp exit successfully in the same scenario? A difference is that this is recoverable and that's not. I don't know what we would do with the NEL in IOApp other than exit non-zero and maybe log the (probably uselessly toStringed) runnables.
I bring it up here, wondering whether this should be nested or has other uses. But even if we change the behavior of IOApp, it's hard to see that the exception is useful there.
Sure it will, just pass
You mean specifically the constructor that creates a thread pool? I'm OK with moving that to JVM only.
FS2 has a couple of places in the core project that took a Again, this isn't a new type class -- there's already |
|
I moved the JVM specific constructors to JVM source directory: 57edf53 |
That's up to whoever instantiates
This is not a typeclass. The discussion of the conflation of the two concerns (a concrete datatype and a set of more abstract typeclasses) is a real one with a lot of nuance and merit, and IMO should be continued on the cats-effect 3 issue. We are where we are today, however.
There is no runtime blow-up. I'll buy the clutter argument, but there's no need for hyperbole.
Conditionally-compiled APIs is one of the worst things ever, particularly with top-level types. Conditionally-available functions are a little better, since for the most part you can just avoid calling them, but making I think that perhaps wrapping the
Oh it would be… if it were in the typeclass hierarchy. Which it is not. Which you know. Please keep this discussion focused on the merits of this change, rather than on broader philosophical points. As I said, I'm interested in the discussion you're referencing, and I think it should be continued… on the cats-effect 3 discussion issue. Not here.
Cats Effect 2 is an interim step. Cats Effect 3 will break many of these things, likely including The insinuation of carelessness is also unwelcome, here and everywhere. If I had heard significant objection to |
Yes, exactly.
I am happy to see that.
I can see the convenience, although shouldn't that be an implementation detail of a
No hyperbole intended. By "blow up", I mean there were methods in the pull request that would throw an exception if invoked on Scala.js. We have all the tools necessary to restrict JVM-only features to the JVM, and IMO we should use them (and I am happy to see 5c073ef).
Whether type class or not, this is a chunk of functionality whose value is intrinsically tied to use of the concrete Cats IO data type, and I bring it up because adding even more such IO-specific functionality into Cats Effect should be explicitly recognized as a design-smell.
I never said it was careless—I said it was rushed, which is not the same as careless. In the past, changes this large have taken more time and have involved input from Alex, myself, or both, and have considered more aspects of the ecosystem (such as Scala.js compatibility, utility for Monix and ZIO, etc.). I feel if I had not seen this today (more or less chance!), it would already be merged in, without separation of JVM-only functionality, and this would have had a lasting impact on the API. |
You seem to conflate "ZIO does not need it" with "it's needed only for cats IO", forgetting polymorphic code, which is a primary design concern for cats-effect |
|
IMO a true effect-polymorphic solution would introduce a |
Sure, but if you consider a bit of context you see that is just an improvement over current I'm sure |
Well, this is precisely the kind of conversation I would have liked to have, or at least see someone else have. The following design would be truly effect polymorphic (i.e. no assumptions about implementation) and of use to non-IO types, as well as fully implementable for Cats IO: trait Blocking[F[_]] {
def blocking[A](fa: F[A]): F[A]
}It also has better ergonomics and is easier to constrain to the JVM.
Since we are literally talking about a JVM-only thread pool that has unbounded capacity, I don't think that argument has merit in the current context. |
This is a new type class, which would require all effect libraries to opt-in. This is beyond the scope of 2.0 IMO.
Note you're focused on the
Deadlines tend to produce PRs and this is by no means even close to the largest PR to hit just prior to a deadline. I'd describe this as a small PR, adding a newtype around More importantly, cats-effect issues tend to be bike shed more than any project I've seen. I worry that we are driving away contributors and current maintainers with these conversations. |
I don't want to replace That said, fair enough!
This is exactly the kind of discussion I want to have. I'd point out that
I agree, and I saw similar urgency prior to CE 1.0 and appreciated the pushbacks.
Although you would expect and hope that in a widely depended upon library, changes would involve discussion and take some time to work through. But you may be right about the bike-shedding, and I would propose the following: Stop bike-shedding at the request of any Cats Effect contributor when any pull request / issue has:
The stake holders are the end-users, the library maintainers, and the effect maintainers, and if you have a little consensus in each category, it's probably a safe enough change to push through without additional bike-shedding. |
While I think that is true to a degree, it can also cause some significant heartache in a lot of cases when not done carefully, particularly when employed on top-level types. In this case, simply moving the constructor is certainly the right approach.
You continue to see Cats Effect as "abstraction primary, concrete secondary", and thus you view any API changes to Cats Effect which are not literally part of To be clear, I'm not begrudging you your biases. I certainly have my own. What I am begrudging is your assumption that everyone shares yours.
I certainly considered Scala.js while reviewing this, though perhaps not with the thoroughness that I should have. Monix and ZIO always have a seat at this table (as does everyone, really), but in this case the input is less relevant. Alex is also extremely busy right now.
Probably not merged today, but if you hadn't seen it at all and no one else had raised the point about the thread pool constructor, then yes it likely would have been merged in that form. And that is precisely why I engage in these sorts of conversations: deep down, at the end of the day, some value will come out of it.
Drifting into cats-effect 3 land, but unless datatypes have an intended implementation of
I can think of at least three cases within as many years where I have needed non-singleton pools for blocking tasks. At least one of them you were personally witness to. It's very common, particularly when interacting with older APIs written for the Java Enterprise Application Server era.
It is.
This. Very much this. While I don't mind a lot of reasoned debate and careful consideration around even minor things like naming on a project which is as central as cats-effect, there is certainly a line that we need to be careful of.
I more or less follow this process already when policing this community (granted, from which I took a long hiatus). The exception that I draw is that, on certain matters I lend considerably more weight to implementors of datatypes, on certain others I lend considerably more weight to middleware consumers (polymorphic libraries), and on a third set I lend more weight to end-user considerations (downstream consumers of polymorphic libraries and datatypes). Cats Effect's role is to mediate between all three of these, and some issues lend considerably more credence to one stakeholder over another. But in general, yes, I think that if something has overwhelming and universal buy-in of the type you describe, then it is beyond a shadow of a doubt. |
My biases, whatever they may be, don't invalidate my technical arguments that In fact, if at any point we find ourselves discussing other people's "biases" and "misconceptions" (which are personal judgments), rather than discussing technical issues, then I'd say we've veered off the happy path and should discontinue the conversation. |
| * | ||
| * Instances of this class should *not* be passed implicitly. | ||
| */ | ||
| final class Blocker private (val blockingContext: ExecutionContext) { |
kubukoz
Jun 11, 2019
Member
Why not a value class?
Why not a value class?
mpilquist
Jun 11, 2019
Author
Member
No reason really - I tend to avoid value classes unless the performance gain is significant. In this case, it's a single extra allocation per app in most cases. :)
No reason really - I tend to avoid value classes unless the performance gain is significant. In this case, it's a single extra allocation per app in most cases. :)
kubukoz
Jun 11, 2019
Member
Good point, OTOH it'd make sense if old-style evalOn was removed. But since it's not being removed... :)
Good point, OTOH it'd make sense if old-style evalOn was removed. But since it's not being removed... :)
| * @param blocker blocker where the evaluation has to be scheduled | ||
| * @param fa Computation to evaluate using `blocker` | ||
| */ | ||
| def evalOn[A](blocker: Blocker)(fa: F[A]): F[A] = |
kubukoz
Jun 11, 2019
Member
overloading is still something I'd try to avoid as long as possible... If this was blockOn, it'd probably make Blocker eligible for a value class, no? Plus more explicit than just an overload
overloading is still something I'd try to avoid as long as possible... If this was blockOn, it'd probably make Blocker eligible for a value class, no? Plus more explicit than just an overload
|
Has anyone mentioned the possibility of taking the required type classes (Sync, ContextShift [yes I know it's not a type class]) at construction time, instead of at call sites of methods? That'd make Blocker[F] a thing and possibly reduce the amount of Sync necessary to use it in many places. And it wouldn't push any responsibility on the implementations like Monix and ZIO either. |
|
@kubukoz That was intentional, as it causes problems when using multiple effects -- e.g. transformers plus a base effect type. The resource itself (the pool) doesn't care which |
|
@mpilquist I see, so how about dual effects like in creation of a Ref and friends? |
|
Same issue still -- e.g. imagine a Skunk app that uses a |
|
|
|
Then you run in to an issue with |
|
BTW, you also run in to the issue of changing which implicits have to be passed to various use sites -- something |
Fixes #555