-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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] attempt to use external submission for rescheduling of actor mailboxes #31156
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,9 +5,10 @@ | |
package akka.dispatch | ||
|
||
import java.util.concurrent.{ ExecutorService, ForkJoinPool, ForkJoinTask, ThreadFactory } | ||
|
||
import com.typesafe.config.Config | ||
|
||
import java.lang.invoke.MethodHandles | ||
|
||
object ForkJoinExecutorConfigurator { | ||
|
||
/** | ||
|
@@ -28,11 +29,22 @@ object ForkJoinExecutorConfigurator { | |
|
||
override def execute(r: Runnable): Unit = | ||
if (r ne null) | ||
super.execute( | ||
(if (r.isInstanceOf[ForkJoinTask[_]]) r else new AkkaForkJoinTask(r)).asInstanceOf[ForkJoinTask[Any]]) | ||
super.execute(createTask(r)) | ||
else | ||
throw new NullPointerException("Runnable was null") | ||
|
||
def executeExternal(r: Runnable): Unit = | ||
handle.invokeWithArguments(createTask(r)) | ||
|
||
private val handle = { | ||
val m = classOf[ForkJoinPool].getDeclaredMethod("externalPush", classOf[ForkJoinTask[_]]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's hope that this method is available on all supported JDKs... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like it was there already in 1.8 at least: https://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/concurrent/ForkJoinPool.java#l1481 |
||
m.setAccessible(true) | ||
MethodHandles.lookup().unreflect(m).bindTo(this) | ||
} | ||
|
||
private def createTask(r: Runnable): ForkJoinTask[Any] = | ||
(if (r.isInstanceOf[ForkJoinTask[_]]) r else new AkkaForkJoinTask(r)).asInstanceOf[ForkJoinTask[Any]] | ||
|
||
def atFullThrottle(): Boolean = this.getActiveThreadCount() >= this.getParallelism() | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -232,7 +232,7 @@ private[akka] abstract class Mailbox(val messageQueue: MessageQueue) | |
} | ||
} finally { | ||
setAsIdle() //Volatile write, needed here | ||
dispatcher.registerForExecution(this, false, false) | ||
dispatcher.registerForExecution(this, false, false, true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the magic improvement where we say that we want to "reschedule" the mailbox, i.e. we yield because this actor has reached its throughput. |
||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,8 @@ | |
|
||
package akka.dispatch | ||
|
||
import akka.dispatch.ForkJoinExecutorConfigurator.AkkaForkJoinPool | ||
|
||
import java.util.Collection | ||
import java.util.concurrent.{ | ||
ArrayBlockingQueue, | ||
|
@@ -21,7 +23,6 @@ import java.util.concurrent.{ | |
TimeUnit | ||
} | ||
import java.util.concurrent.atomic.{ AtomicLong, AtomicReference } | ||
|
||
import scala.concurrent.{ BlockContext, CanAwait } | ||
import scala.concurrent.duration.Duration | ||
|
||
|
@@ -217,6 +218,10 @@ trait ExecutorServiceDelegate extends ExecutorService { | |
def executor: ExecutorService | ||
|
||
def execute(command: Runnable) = executor.execute(command) | ||
def executeExternal(command: Runnable) = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is public API, so not sure if would want to add that method here publicly? |
||
if (executor.isInstanceOf[AkkaForkJoinPool]) | ||
executor.asInstanceOf[AkkaForkJoinPool].executeExternal(command) | ||
else executor.execute(command) | ||
|
||
def shutdown(): Unit = { executor.shutdown() } | ||
|
||
|
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.
registerForExecution
is called from 3 places. Do we need the extra parameter or can we handle all of them as executeExternal?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.
That's one of the remaining questions. We would have to benchmark to be sure. The idea of this approach is that we only use external submission if an actor exhausted its throughput batch. This way all other submissions (e.g. sending a message to another actor) could still be scheduled on the local queue. Also, all the other calls would use a normal method invocation for scheduling (instead of a reflective method handle invocation) which might have a performance benefit.
Whether the solution in this PR is good enough is another question, because it would mean that a busy loop involving two or more actors could still starve a pool thread. But that's the whole issue here: we would have to define a sensible fairness metric and experiment if optimizing for that fairness metric would make a difference in certain scenarios. After all, right now fairness is based on "number of message processed" (by setting
throughput
) which may or may not lead to a sensible behavior (and which is easy to enough to "exploit" accidentally by blocking or long running CPU-intensive tasks).