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 80048b7aa1 fix: preserve Runnable hash codes in PekkoForkJoinTask 
(#3668)
80048b7aa1 is described below

commit 80048b7aa134c8fa7f8b515edafa1bdb038de649
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Fri Oct 23 16:21:51 2026 +0800

    fix: preserve Runnable hash codes in PekkoForkJoinTask (#3668)
    
    Motivation:
    Fresh fork-join task wrappers do not preserve their Runnable's hash code.
    
    Modification:
    Delegate hashCode to the Runnable with a final override and document the
    stability requirement. Cover hash boundaries and independent wrapper
    identity, completion, and cancellation in MailboxTaskSubmissionSpec.
    
    Result:
    Fresh wrappers retain the Runnable's hash code while keeping separate
    fork-join task state.
    
    Tests:
    - New regressions: 2 failures before the fix; all 6 suite tests pass after
      the fix on Scala 2.13.18 and 3.3.8, JDK 17.
    - All 6 suite tests also passed on Scala 2.13.18, JDK 25.0.2.
    - Related dispatcher/mailbox/fork-join suites: 73 passed, 2 JDK-dependent
      pending tests.
    - Native scalafmt, headerCreateAll, checkCodeStyle including
      +headerCheckAll, and git diff --check passed.
    - +mimaReportBinaryIssues passed on Scala 2.13.18 and 3.3.8.
    - sortImports hit an existing SortImports/Scalafix NoSuchMethodError;
      unrelated generated changes were discarded.
    - validatePullRequest was stopped after a TcpConnectionSpec timeout.
      Isolated rerun: 35 passed, 1 ignored, 1 pending, without code changes.
    - Auditable Qoder stdout review and follow-up: no must-fix findings.
    
    References:
    Refs #3621, Refs #3628.
---
 .../pekko/dispatch/MailboxTaskSubmissionSpec.scala | 47 +++++++++++++++++++++-
 .../dispatch/ForkJoinExecutorConfigurator.scala    |  7 ++++
 2 files changed, 53 insertions(+), 1 deletion(-)

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 ad731104fc..597d9b8261 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
@@ -21,7 +21,7 @@ import java.util.concurrent.{ ForkJoinPool, ForkJoinTask, 
ForkJoinWorkerThread }
 import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger }
 
 import org.apache.pekko
-import pekko.dispatch.ForkJoinExecutorConfigurator.PekkoForkJoinPool
+import pekko.dispatch.ForkJoinExecutorConfigurator.{ PekkoForkJoinPool, 
PekkoForkJoinTask }
 import pekko.testkit.PekkoSpec
 
 /**
@@ -75,6 +75,51 @@ class MailboxTaskSubmissionSpec extends PekkoSpec {
     }
   }
 
+  "PekkoForkJoinTask" must {
+
+    "preserve the Runnable hash code across fresh wrappers" in {
+      for (hash <- Seq(Int.MinValue, -1, 0, 1, Int.MaxValue)) {
+        val runnable = new Runnable {
+          final override def run(): Unit = ()
+          final override def hashCode(): Int = hash
+        }
+        val first = new PekkoForkJoinTask(runnable)
+        val second = new PekkoForkJoinTask(runnable)
+
+        withClue(s"Runnable hash code $hash: ") {
+          first.hashCode() shouldBe hash
+          second.hashCode() shouldBe hash
+          (first should not).equal(second)
+        }
+      }
+    }
+
+    "retain independent completion and cancellation for wrappers with the same 
hash code" in {
+      val runs = new AtomicInteger(0)
+      val runnable = new Runnable {
+        final override def run(): Unit = { runs.incrementAndGet() }
+        final override def hashCode(): Int = 42
+      }
+      val cancelled = new PekkoForkJoinTask(runnable)
+      val completed = new PekkoForkJoinTask(runnable)
+      val pending = new PekkoForkJoinTask(runnable)
+
+      cancelled.cancel(false) shouldBe true
+      completed.invoke()
+
+      cancelled.isCancelled shouldBe true
+      completed.isCompletedNormally shouldBe true
+      pending.isDone shouldBe false
+      runs.get() shouldBe 1
+      Set(cancelled, completed, pending).size shouldBe 3
+      Seq(cancelled, completed, pending).foreach(_.hashCode() shouldBe 42)
+
+      pending.invoke()
+      pending.isCompletedNormally shouldBe true
+      runs.get() shouldBe 2
+    }
+  }
+
   "PekkoForkJoinPool" must {
 
     "not run a cancelled ForkJoinTask submitted as a Runnable" in {
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 5b056398fb..387b78a554 100644
--- 
a/actor/src/main/scala/org/apache/pekko/dispatch/ForkJoinExecutorConfigurator.scala
+++ 
b/actor/src/main/scala/org/apache/pekko/dispatch/ForkJoinExecutorConfigurator.scala
@@ -77,6 +77,13 @@ object ForkJoinExecutorConfigurator {
    */
   @SerialVersionUID(1L)
   final class PekkoForkJoinTask(runnable: Runnable) extends ForkJoinTask[Unit] 
{
+
+    /**
+     * Preserve the wrapped Runnable's hash code across fresh task wrappers, 
provided that the
+     * Runnable's hash code is stable. Each wrapper retains its own identity 
and completion state.
+     */
+    final override def hashCode(): Int = runnable.hashCode()
+
     override def getRawResult(): Unit = ()
     override def setRawResult(unit: Unit): Unit = ()
     override def exec(): Boolean =


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to