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

Revert ExecutionContext.global to not be a BatchingExecutor #9270

Merged
merged 1 commit into from Oct 23, 2020

Conversation

dwijnand
Copy link
Member

@dwijnand dwijnand commented Oct 23, 2020

NOTE This is a change in behaviour. If you make use of scala.concurrent.ExecutionContext.global you may
want to adapt your code. Please note that Akka uses its own execution contexts (dispatchers) so is
unaffected.

In 2.13.0 we changed the behaviour of ExecutionContext.global so that it was more "opportunistic", by
attempting to batch nested task and execute them on the same thread as the enclosing task. Unfortunately this
relies heavily on the correct usage of the blocking { } wrapper on any long-running and/or blocking tasks;
code that didn't could end up executing serially instead of in parallel. As we believe that correct usage of
blocking isn't as well followed as hoped we decided to revert ExecutionContext.global to its 2.12
behaviour and introduce ExecutionContext.opportunistic for users that want to switch to it.

In order to maintain forwards and backwards binary compatibility of the scala standard library, the usage
instructions require some indirection. Library authors can use the opportunistic execution context for all
Scala 2.13.x versions like so:

implicit val ec: scala.concurrent.ExecutionContext = try {
  scala.concurrent.ExecutionContext.getClass
    .getDeclaredMethod("opportunistic")
    .invoke(scala.concurrent.ExecutionContext)
    .asInstanceOf[scala.concurrent.ExecutionContext]
} catch {
  case _: NoSuchMethodException =>
    scala.concurrent.ExecutionContext.global
}

While application authors just need to gain access to the private[scala] field, which they can do by:

  1. Using a structural type (example below) which uses Java runtime reflection.
  2. Writing a Scala object in the scala package (example below).
  3. Writing a Java source file. This works because private[scala] is emitted as public in Java bytecode.

For example (option 1):

implicit val ec: scala.concurrent.ExecutionContext =
  (scala.concurrent.ExecutionContext:
    {def opportunistic: scala.concurrent.ExecutionContextExecutor}
  ).opportunistic

Links:

Fixes scala/bug#12089

@scala-jenkins scala-jenkins added this to the 2.13.5 milestone Oct 23, 2020
@dwijnand dwijnand modified the milestones: 2.13.5, 2.13.4 Oct 23, 2020
@lrytz lrytz merged commit 79b4579 into scala:2.13.x Oct 23, 2020
@dwijnand dwijnand deleted the revert-batching-global branch October 23, 2020 12:05
@lrytz
Copy link
Member

lrytz commented Oct 23, 2020

Oh *?^!@#?#Q@ I didn't intend to merge this, I was on the wrong tab...

This needs a follow-up at least in documentation, to tell people about the batching alternative and how to get it

  • On ExecutionContext.global, maybe also ExecutionContext.Implicits.global
  • Possibly in the @implicitNotFound on ExecutionContext

Here is how users can create a batching EC instance in their codebase (code adapted from a draft by @viktorklang). Maybe this can be improved further, feedback welcome. 2.13.x...lrytz:custom-batching-ec

We also plan to ship the batching EC in scala-library-next.

If preferred, we can revert this PR and re-submit it.

@viktorklang
Copy link
Contributor

@lrytz I have multiple issues with the proposal:

  1. Have "everyone" reimplement the 2.13 global behavior will reduce performance since batches won't be shared between different implementations
  2. Users who are depending on third-party library code which uses global have little chance of regaining lost performance since their dependency might have hard-coded the use of EC.global
  3. Implementing the 2.13 global behavior requires access to internal code, which will not be under binary compatibility guarantees (relying om implementation details)
  4. Users who lose performance due to the revert will likely only incidentally realize the cause
  5. Putting the 2.13 global behavior in scala-library-next has very limited discoverability and I'd wager that it will see little use (although I hope I am wrong)

@lrytz
Copy link
Member

lrytz commented Oct 26, 2020

Have "everyone" reimplement the 2.13 global behavior will reduce performance since batches won't be shared between different implementations

Users who are depending on third-party library code which uses global have little chance of regaining lost performance since their dependency might have hard-coded the use of EC.global

My assumption is that the actual EC used in an application is always imported (or defined) by the application's own source code - is that wrong? Are there libraries out there that use an EC internally without requiring it to be provided as parameter? To me this would seem to be a problematic design; any application that doesn't use global would run with multiple ECs.

Implementing the 2.13 global behavior requires access to internal code, which will not be under binary compatibility guarantees (relying om implementation details)

Yes, we're making the BatchingExecutor and Promise.Transformation classes accessible and we'll have to guarantee binary compatibility. The accessible members are

  • Promise.Transformation: public def benefitsFromBatching
  • BatchingExecutor: protected def submitForExecution, protected def reportFailure, protected final def submitAsyncBatched, protected final def submitSyncBatched

This looks reasonable to me, the risk of it causing problems in 2.13.x maintenance seems really low.

Users who lose performance due to the revert will likely only incidentally realize the cause

Putting the 2.13 global behavior in scala-library-next has very limited discoverability and I'd wager that it will see little use (although I hope I am wrong)

I agree this is a concern, but in my opinion the benefits of reverting global to its 2.12 behavior outweigh. Of course we will highlight the change in the announcements and release notes. We can work through documentation that we own to include references.

@dwijnand dwijnand added the release-notes worth highlighting in next release notes label Oct 26, 2020
@viktorklang
Copy link
Contributor

My assumption is that the actual EC used in an application is always imported (or defined) by the application's own source code - is that wrong? Are there libraries out there that use an EC internally without requiring it to be provided as parameter? To me this would seem to be a problematic design; any application that doesn't use global would run with multiple ECs.

What people are recommended to do, and what they actually do, was what ended us up in this situation in the first place :)

Yes, we're making the BatchingExecutor and Promise.Transformation classes accessible and we'll have to guarantee binary compatibility. The accessible members are

BatchingExecutor might not be an issue, but leaking internal Promise implementation details is a bit of a no-starter since it completely prevents the implementation to evolve. Also consider ScalaJS.

I agree this is a concern, but in my opinion the benefits of reverting global to its 2.12 behavior outweigh. Of course we will highlight the change in the announcements and release notes. We can work through documentation that we own to include references.

I hope you're right!

@lrytz
Copy link
Member

lrytz commented Oct 27, 2020

Two more things we can do:

  • Add the batching EC in a private[scala] field in 2.13.4
    • Application users (those not publishing a library) that have control over the Scala version at runtime could access that (there are ways to do so)
    • It could come in handy in the future, for example if there is more tooling in place so that a library could require a minimal Scala version
  • Mark the new public type aliases @deprecated with a clear message, and silence the depreaction in the code snippet for a user-defined batching EC with @nowarn

@viktorklang
Copy link
Contributor

@lrytz Adding the opportunistic EC (Batching on top of global) as a private[scala] sounds good. A more ideal solution would be if one could have it be public, but with an @backwardsIncompatible annotation which would alert users that if they depend on it, it will not work with an older scala-lib (or even better, if an @since-annotation whose "minor" (epoch.major.minor) is not a "0" would automatically warn on compilation warning that using it means that the app is no longer backwards-compatible with an older scala-lib)

@lrytz
Copy link
Member

lrytz commented Oct 28, 2020

@backwardsIncompatible

This could be done with #8820

@dwijnand dwijnand mentioned this pull request Oct 28, 2020
72 tasks
@viktorklang
Copy link
Contributor

@dwijnand @lrytz Do you want me to open a PR with the private[scala] opportunistic EC so we can include it in 2.13.4?

@lrytz
Copy link
Member

lrytz commented Oct 29, 2020

That'd be welcome, thank you!

@viktorklang
Copy link
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes worth highlighting in next release notes
Projects
None yet
4 participants