This is an automated email from the ASF dual-hosted git repository.
He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git
The following commit(s) were added to refs/heads/main by this push:
new 77b5c1883a Revert unconditional fork-join task wrapping from #3621
(#3628)
77b5c1883a is described below
commit 77b5c1883a910d37d531b43c8537f9879e49a84c
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Thu Oct 15 19:22:00 2026 +0800
Revert unconditional fork-join task wrapping from #3621 (#3628)
* Revert unconditional fork-join task wrapping from #3621
Motivation:
Making Mailbox a plain Runnable in #3621 already gives each mailbox
submission a fresh task. Its unrelated executor change can bypass an
existing ForkJoinTask's cancellation and leave a custom task incomplete,
causing join() to wait indefinitely.
Modification:
Restore the executor implementation exactly as it was before #3621.
Preserve the Mailbox fix, MiMa filters, and PekkoForkJoinTask's handling
of plain Runnable submissions.
Add cancellation and bounded completion regressions, move the termination
assertion out of finally, and clarify the plain-Runnable reuse control.
Result:
Mailboxes retain fresh task identities while caller-owned fork-join
tasks retain their completion and cancellation semantics. Interruption
handling for wrapped plain Runnables is unchanged.
Tests:
- MailboxTaskSubmissionSpec: 4 passed on Scala 2.13.18 and 3.3.8, JDK 17.
- The same four tests against #3621: cancellation and completion failed
as expected; both controls passed.
- Related dispatcher/mailbox/fork-join/ActorSystem suites: 189 passed,
2 existing pending tests on Scala 2.13.18, JDK 17.
- Native scalafmt, headerCreateAll, +headerCheckAll, checkCodeStyle,
and git diff --check passed.
- +mimaReportBinaryIssues passed on Scala 2.13.18 and 3.3.8.
- validatePullRequest did not complete on this ARM Mac: two TCP tests
timed out, and persistence's LevelDB JNI only contains x86_64/i386
native libraries. Stopped the remaining validation after that failure.
- Isolated TcpConnectionSpec and TcpListenerSpec rerun: 42 passed,
1 existing ignored test and 1 existing pending test.
- Auditable Qoder stdout review: no code-level must-fix findings.
The reviewed working-tree changes are included in this amended commit.
References:
Refs #3621.
* Update
actor/src/main/scala/org/apache/pekko/dispatch/ForkJoinExecutorConfigurator.scala
Co-authored-by: PJ Fanning <[email protected]>
---------
Co-authored-by: PJ Fanning <[email protected]>
---
.../pekko/dispatch/MailboxTaskSubmissionSpec.scala | 53 +++++++++++++---------
.../dispatch/ForkJoinExecutorConfigurator.scala | 11 +++--
2 files changed, 38 insertions(+), 26 deletions(-)
diff --git
a/actor-tests/src/test/scala/org/apache/pekko/dispatch/MailboxTaskSubmissionSpec.scala
b/actor-tests/src/test/scala/org/apache/pekko/dispatch/MailboxTaskSubmissionSpec.scala
index bb36d475fa..ad731104fc 100644
---
a/actor-tests/src/test/scala/org/apache/pekko/dispatch/MailboxTaskSubmissionSpec.scala
+++
b/actor-tests/src/test/scala/org/apache/pekko/dispatch/MailboxTaskSubmissionSpec.scala
@@ -33,8 +33,8 @@ import pekko.testkit.PekkoSpec
* runs again. See https://github.com/ben-manes/caffeine/issues/2021 and
* https://github.com/apache/pekko/issues/3620.
*
- * Neither race can be forced from a test, so this pins the two properties
that make them
- * unreachable: the mailbox is not a `ForkJoinTask`, and the pool never
receives one.
+ * This pins the mailbox's plain Runnable shape and resubmission after a
failing run. Existing
+ * ForkJoinTasks submitted as Runnables must retain their completion and
cancellation semantics.
*/
class MailboxTaskSubmissionSpec extends PekkoSpec {
@@ -51,18 +51,17 @@ class MailboxTaskSubmissionSpec extends PekkoSpec {
}
}
- /** The shape the pre-fix `Mailbox` had: resubmitted, `exec()` never
completes it, and rethrows. */
- private final class ReusedTask(body: Runnable) extends ForkJoinTask[Unit]
with Runnable {
+ /** Exposes a Runnable entry point so execute(Runnable) can receive an
existing ForkJoinTask. */
+ private final class RunnableForkJoinTask extends ForkJoinTask[Unit] with
Runnable {
val runs = new AtomicInteger(0)
override def getRawResult(): Unit = ()
override def setRawResult(unit: Unit): Unit = ()
override def exec(): Boolean = {
run()
- false
+ true
}
override def run(): Unit = {
runs.incrementAndGet()
- body.run()
}
}
@@ -78,37 +77,49 @@ class MailboxTaskSubmissionSpec extends PekkoSpec {
"PekkoForkJoinPool" must {
- // The mailbox is submitted through ExecutorService.execute(Runnable), so
cast to keep the
- // overload resolution identical to the dispatcher's call site.
- "run a reused ForkJoinTask again after one of its runs threw" in {
+ "not run a cancelled ForkJoinTask submitted as a Runnable" in {
val pool = newPool()
+ val task = new RunnableForkJoinTask
try {
- val firstRunFailed = new AtomicBoolean(false)
- val task = new ReusedTask(new Runnable {
- override def run(): Unit =
- if (firstRunFailed.compareAndSet(false, true)) throw new
RuntimeException("expected test failure")
- })
-
+ task.cancel(false) shouldBe true
+ // Select execute(Runnable), which must preserve the existing task's
cancellation state.
pool.execute(task: Runnable)
- awaitAssert(task.runs.get() shouldBe 1)
+ } finally pool.shutdown()
+ pool.awaitTermination(remainingOrDefault.toMillis,
java.util.concurrent.TimeUnit.MILLISECONDS) shouldBe true
+ task.isCancelled shouldBe true
+ task.runs.get() shouldBe 0
+ }
+ "complete a ForkJoinTask submitted as a Runnable" in {
+ val pool = newPool()
+ val task = new RunnableForkJoinTask
+ try {
pool.execute(task: Runnable)
- // Before the fix the task object itself was pushed, the throw
completed it exceptionally,
- // and ForkJoinTask.doExec short-circuits on a negative status, so
this stayed at 1.
- awaitAssert(task.runs.get() shouldBe 2)
+ // Bound the wait so a regression fails instead of hanging in join().
+ awaitAssert(task.isDone shouldBe true)
+ task.isCompletedNormally shouldBe true
+ task.join()
+ task.runs.get() shouldBe 1
} finally pool.shutdown()
}
- "still run a plain reused Runnable" in {
+ // Control: plain Runnables receive a fresh wrapper on every submission,
including before #3621.
+ "run a plain reused Runnable again after one of its runs threw" in {
val pool = newPool()
try {
val runs = new AtomicInteger(0)
+ val firstRunFailed = new AtomicBoolean(false)
val task = new Runnable {
- override def run(): Unit = runs.incrementAndGet()
+ override def run(): Unit = {
+ runs.incrementAndGet()
+ if (firstRunFailed.compareAndSet(false, true)) throw new
RuntimeException("expected test failure")
+ }
}
pool.execute(task)
awaitAssert(runs.get() shouldBe 1)
+ // Let the first wrapper finish failing before checking a subsequent
submission.
+ awaitAssert(pool.isQuiescent shouldBe true)
pool.execute(task)
awaitAssert(runs.get() shouldBe 2)
} finally pool.shutdown()
diff --git
a/actor/src/main/scala/org/apache/pekko/dispatch/ForkJoinExecutorConfigurator.scala
b/actor/src/main/scala/org/apache/pekko/dispatch/ForkJoinExecutorConfigurator.scala
index 870a59cf0d..5b056398fb 100644
---
a/actor/src/main/scala/org/apache/pekko/dispatch/ForkJoinExecutorConfigurator.scala
+++
b/actor/src/main/scala/org/apache/pekko/dispatch/ForkJoinExecutorConfigurator.scala
@@ -60,11 +60,12 @@ object ForkJoinExecutorConfigurator {
override def execute(r: Runnable): Unit =
if (r ne null)
- // Always hand the pool a task object it has not seen before. Pushing
a caller-owned
- // ForkJoinTask lets the same reference return to the same work-queue
slot, which a steal
- // that stalls between its base recheck and its slot CAS turns into a
permanently damaged
- // queue: https://github.com/ben-manes/caffeine/issues/2021
- super.execute(new PekkoForkJoinTask(r))
+ // An existing ForkJoinTask is pushed as-is (matching
ForkJoinPool.execute) so that its own
+ // completion and cancellation state is honoured. Callers must not
resubmit the same task
+ // instance (see https://github.com/ben-manes/caffeine/issues/2021);
this is why Mailbox
+ // is deliberately not a ForkJoinTask.
+ super.execute(
+ (if (r.isInstanceOf[ForkJoinTask[?]]) r else new
PekkoForkJoinTask(r)).asInstanceOf[ForkJoinTask[Any]])
else
throw new NullPointerException("Runnable was null")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]