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 89b590f65d Avoid receive timeout type pollution (#3332)
89b590f65d is described below

commit 89b590f65d0a31dbc5c2855a92136bef86b16f6f
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Tue Jul 21 19:58:36 2026 +0800

    Avoid receive timeout type pollution (#3332)
    
    * fix: avoid receive timeout type pollution
    
    Motivation:
    The ActorCell receive timeout path allocates on every message
    and misclassifies timer messages, causing unnecessary timeout
    rescheduling and state churn.
    
    Modification:
    - Avoid type pollution in the receive timeout hot path
    - Guard timer receive timeout classification
    
    Result:
    Reduced allocations and correct timeout behavior for actors
    using receive timeouts.
    
    Tests:
    - Existing ActorCellReceiveTimeoutSpec
    
    References:
    Refs #1668
    
    * Remove @inline annotation for Scala 3 compatibility
    
    Motivation:
    The @inline annotation does not work for Scala 3. The JVM JIT will inline
    this small method regardless of the annotation.
    
    Modification:
    Remove the @inline annotation from isNotInfluenceReceiveTimeout method.
    
    Result:
    Code compiles cleanly on both Scala 2.13 and Scala 3. Performance is
    unaffected as the JIT compiler will still inline the method.
    
    Tests:
    Not run - annotation removal only, no behavior change
    
    References:
    Refs #3332
    
    * Split isNotInfluenceReceiveTimeout into Scala 2/3 version-specific files
    
    Motivation:
    The @inline annotation does not work for Scala 3. Reviewer requested
    splitting the method into version-specific files to preserve the inline
    optimization for Scala 2 while supporting Scala 3.
    
    Modification:
    - Create ReceiveTimeoutCompat object in scala-2.13 with @inline annotation
    - Create ReceiveTimeoutCompat object in scala-3 with inline keyword
    - Move isNotInfluenceReceiveTimeout method from ReceiveTimeout to 
ReceiveTimeoutCompat
    - Update all call sites to use ReceiveTimeoutCompat
    
    Result:
    Code compiles cleanly on both Scala 2.13 and Scala 3 with appropriate
    inline optimizations for each version.
    
    Tests:
    sbt "actor-tests/Test/testOnly org.apache.pekko.actor.ReceiveTimeoutSpec"
    All 14 tests passed.
    
    References:
    Refs #3332
    
    * fix: update references to ReceiveTimeoutCompat after method relocation
    
    Motivation:
    The isNotInfluenceReceiveTimeout method was moved from ReceiveTimeout to
    ReceiveTimeoutCompat in the version-specific files, but two call sites
    still referenced the old location, causing compilation failures across
    all CI jobs.
    
    Modification:
    Update TimerSchedulerImpl (actor-typed) and 
ReceiveTimeoutTypePollutionBenchmark
    (bench-jmh) to reference ReceiveTimeoutCompat.isNotInfluenceReceiveTimeout.
    
    Result:
    All modules compile successfully on both Scala 2.13 and Scala 3.
    
    Tests:
    - sbt "actor-typed / Compile / compileIncremental" (Scala 2.13) - success
    - sbt "++3.3.8; actor-typed / Compile / compileIncremental" - success
    
    References:
    Refs #3332
---
 .../apache/pekko/actor/ReceiveTimeoutSpec.scala    |  7 ++
 .../actor/typed/internal/TimerSchedulerImpl.scala  |  2 +-
 .../pekko/actor/dungeon/ReceiveTimeoutCompat.scala | 25 +++++++
 .../pekko/actor/dungeon/ReceiveTimeoutCompat.scala | 25 +++++++
 .../scala/org/apache/pekko/actor/ActorCell.scala   |  2 +-
 .../pekko/actor/dungeon/ReceiveTimeout.scala       | 17 +++--
 .../pekko/actor/dungeon/TimerSchedulerImpl.scala   |  2 +-
 .../ReceiveTimeoutTypePollutionBenchmark.scala     | 81 ++++++++++++++++++++++
 8 files changed, 152 insertions(+), 9 deletions(-)

diff --git 
a/actor-tests/src/test/scala/org/apache/pekko/actor/ReceiveTimeoutSpec.scala 
b/actor-tests/src/test/scala/org/apache/pekko/actor/ReceiveTimeoutSpec.scala
index 1aa2e3d43f..ef4ecb344f 100644
--- a/actor-tests/src/test/scala/org/apache/pekko/actor/ReceiveTimeoutSpec.scala
+++ b/actor-tests/src/test/scala/org/apache/pekko/actor/ReceiveTimeoutSpec.scala
@@ -21,6 +21,7 @@ import scala.concurrent.Await
 import scala.concurrent.duration._
 
 import org.apache.pekko
+import pekko.actor.dungeon.{ ReceiveTimeoutCompat => ReceiveTimeoutSupport }
 import pekko.testkit._
 
 object ReceiveTimeoutSpec {
@@ -80,6 +81,12 @@ class ReceiveTimeoutSpec extends PekkoSpec() {
 
   "An actor with receive timeout" must {
 
+    "classify messages that do not influence the timeout" in {
+      ReceiveTimeoutSupport.isNotInfluenceReceiveTimeout(Identify(None)) 
should ===(true)
+      ReceiveTimeoutSupport.isNotInfluenceReceiveTimeout(TransparentTick) 
should ===(true)
+      ReceiveTimeoutSupport.isNotInfluenceReceiveTimeout(Tick) should 
===(false)
+    }
+
     "get timeout" taggedAs TimingTest in {
       val timeoutLatch = TestLatch()
 
diff --git 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/TimerSchedulerImpl.scala
 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/TimerSchedulerImpl.scala
index 8672770e2e..a8e57f17cb 100644
--- 
a/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/TimerSchedulerImpl.scala
+++ 
b/actor-typed/src/main/scala/org/apache/pekko/actor/typed/internal/TimerSchedulerImpl.scala
@@ -117,7 +117,7 @@ import org.slf4j.Logger
     val nextGen = timerGen.next()
 
     val timerMsg =
-      if (msg.isInstanceOf[NotInfluenceReceiveTimeout])
+      if 
(pekko.actor.dungeon.ReceiveTimeoutCompat.isNotInfluenceReceiveTimeout(msg))
         new TimerMsg(key, nextGen, this) with NotInfluenceReceiveTimeout
       else
         new TimerMsg(key, nextGen, this)
diff --git 
a/actor/src/main/scala-2.13/org/apache/pekko/actor/dungeon/ReceiveTimeoutCompat.scala
 
b/actor/src/main/scala-2.13/org/apache/pekko/actor/dungeon/ReceiveTimeoutCompat.scala
new file mode 100644
index 0000000000..035ef7fae4
--- /dev/null
+++ 
b/actor/src/main/scala-2.13/org/apache/pekko/actor/dungeon/ReceiveTimeoutCompat.scala
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package org.apache.pekko.actor.dungeon
+
+import org.apache.pekko.actor.Identify
+import org.apache.pekko.actor.NotInfluenceReceiveTimeout
+
+private[pekko] object ReceiveTimeoutCompat {
+
+  // Identify is also an AutoReceivedMessage. Checking its concrete class 
first avoids polluting its secondary
+  // supertype cache on JDKs affected by JDK-8180450 when actor runtime code 
also checks the AutoReceivedMessage marker.
+  @inline def isNotInfluenceReceiveTimeout(message: Any): Boolean =
+    message.isInstanceOf[Identify] || 
message.isInstanceOf[NotInfluenceReceiveTimeout]
+}
diff --git 
a/actor/src/main/scala-3/org/apache/pekko/actor/dungeon/ReceiveTimeoutCompat.scala
 
b/actor/src/main/scala-3/org/apache/pekko/actor/dungeon/ReceiveTimeoutCompat.scala
new file mode 100644
index 0000000000..c10c616bfe
--- /dev/null
+++ 
b/actor/src/main/scala-3/org/apache/pekko/actor/dungeon/ReceiveTimeoutCompat.scala
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package org.apache.pekko.actor.dungeon
+
+import org.apache.pekko.actor.Identify
+import org.apache.pekko.actor.NotInfluenceReceiveTimeout
+
+private[pekko] object ReceiveTimeoutCompat {
+
+  // Identify is also an AutoReceivedMessage. Checking its concrete class 
first avoids polluting its secondary
+  // supertype cache on JDKs affected by JDK-8180450 when actor runtime code 
also checks the AutoReceivedMessage marker.
+  inline def isNotInfluenceReceiveTimeout(message: Any): Boolean =
+    message.isInstanceOf[Identify] || 
message.isInstanceOf[NotInfluenceReceiveTimeout]
+}
diff --git a/actor/src/main/scala/org/apache/pekko/actor/ActorCell.scala 
b/actor/src/main/scala/org/apache/pekko/actor/ActorCell.scala
index 7d36e05889..6bfbfedebd 100644
--- a/actor/src/main/scala/org/apache/pekko/actor/ActorCell.scala
+++ b/actor/src/main/scala/org/apache/pekko/actor/ActorCell.scala
@@ -561,7 +561,7 @@ private[pekko] class ActorCell(
       }
     finally
       // Schedule or reschedule receive timeout
-      checkReceiveTimeoutIfNeeded(msg, timeoutBeforeReceive)
+      checkReceiveTimeoutIfNeeded(timeoutBeforeReceive)
   }
 
   def autoReceiveMessage(msg: Envelope): Unit = {
diff --git 
a/actor/src/main/scala/org/apache/pekko/actor/dungeon/ReceiveTimeout.scala 
b/actor/src/main/scala/org/apache/pekko/actor/dungeon/ReceiveTimeout.scala
index a8f4a67e65..3484337baa 100644
--- a/actor/src/main/scala/org/apache/pekko/actor/dungeon/ReceiveTimeout.scala
+++ b/actor/src/main/scala/org/apache/pekko/actor/dungeon/ReceiveTimeout.scala
@@ -19,7 +19,7 @@ import scala.concurrent.duration.FiniteDuration
 import org.apache.pekko
 import pekko.actor.ActorCell
 import pekko.actor.Cancellable
-import pekko.actor.NotInfluenceReceiveTimeout
+import pekko.actor.dungeon.ReceiveTimeoutCompat
 
 private[pekko] object ReceiveTimeout {
   final val emptyReceiveTimeoutData: (Duration, Cancellable) = 
(Duration.Undefined, ActorCell.emptyCancellable)
@@ -37,9 +37,11 @@ private[pekko] trait ReceiveTimeout { this: ActorCell =>
   final def setReceiveTimeout(timeout: Duration): Unit = receiveTimeoutData = 
receiveTimeoutData.copy(_1 = timeout)
 
   /** Called after `ActorCell.receiveMessage` or 
`ActorCell.autoReceiveMessage`. */
-  protected def checkReceiveTimeoutIfNeeded(message: Any, beforeReceive: 
(Duration, Cancellable)): Unit =
-    if (hasTimeoutData || receiveTimeoutChanged(beforeReceive))
-      checkReceiveTimeout(!message.isInstanceOf[NotInfluenceReceiveTimeout] || 
receiveTimeoutChanged(beforeReceive))
+  protected def checkReceiveTimeoutIfNeeded(beforeReceive: (Duration, 
Cancellable)): Unit = {
+    val timeoutChanged = receiveTimeoutChanged(beforeReceive)
+    if (hasTimeoutData || timeoutChanged)
+      checkReceiveTimeout(timeoutChanged)
+  }
 
   final def checkReceiveTimeout(reschedule: Boolean): Unit = {
     val (recvTimeout, task) = receiveTimeoutData
@@ -69,10 +71,13 @@ private[pekko] trait ReceiveTimeout { this: ActorCell =>
     receiveTimeoutData ne beforeReceive
 
   protected def cancelReceiveTimeoutIfNeeded(message: Any): (Duration, 
Cancellable) = {
-    if (hasTimeoutData && !message.isInstanceOf[NotInfluenceReceiveTimeout])
+    val beforeReceive = receiveTimeoutData
+    if ((beforeReceive ne emptyReceiveTimeoutData) && 
!ReceiveTimeoutCompat.isNotInfluenceReceiveTimeout(message))
       cancelReceiveTimeoutTask()
 
-    receiveTimeoutData
+    // Returning the state from before cancellation lets 
checkReceiveTimeoutIfNeeded infer whether this message
+    // influenced the timeout without performing the type check a second time.
+    beforeReceive
   }
 
   private[pekko] def cancelReceiveTimeoutTask(): Unit =
diff --git 
a/actor/src/main/scala/org/apache/pekko/actor/dungeon/TimerSchedulerImpl.scala 
b/actor/src/main/scala/org/apache/pekko/actor/dungeon/TimerSchedulerImpl.scala
index e1ea980f3c..45e5b4d77a 100644
--- 
a/actor/src/main/scala/org/apache/pekko/actor/dungeon/TimerSchedulerImpl.scala
+++ 
b/actor/src/main/scala/org/apache/pekko/actor/dungeon/TimerSchedulerImpl.scala
@@ -90,7 +90,7 @@ import pekko.util.OptionVal
     val nextGen = nextTimerGen()
 
     val timerMsg =
-      if (msg.isInstanceOf[NotInfluenceReceiveTimeout])
+      if (dungeon.ReceiveTimeoutCompat.isNotInfluenceReceiveTimeout(msg))
         NotInfluenceReceiveTimeoutTimerMsg(key, nextGen, this)
       else
         InfluenceReceiveTimeoutTimerMsg(key, nextGen, this)
diff --git 
a/bench-jmh/src/main/scala/org/apache/pekko/actor/ReceiveTimeoutTypePollutionBenchmark.scala
 
b/bench-jmh/src/main/scala/org/apache/pekko/actor/ReceiveTimeoutTypePollutionBenchmark.scala
new file mode 100644
index 0000000000..fd3a09cf90
--- /dev/null
+++ 
b/bench-jmh/src/main/scala/org/apache/pekko/actor/ReceiveTimeoutTypePollutionBenchmark.scala
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pekko.actor
+
+import java.util.concurrent.TimeUnit
+
+import org.openjdk.jmh.annotations._
+
+@State(Scope.Thread)
+class ReceiveTimeoutTypePollutionInput {
+  private val messages = Array[AnyRef](Identify("id"), new Object)
+  private var index = 0
+
+  def next(): AnyRef = {
+    index = (index + 1) & 1
+    messages(index)
+  }
+}
+
+@State(Scope.Benchmark)
+@BenchmarkMode(Array(Mode.Throughput))
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Fork(3)
+@Threads(12)
+@Warmup(iterations = 5, time = 1)
+@Measurement(iterations = 5, time = 1)
+class ReceiveTimeoutTypePollutionBenchmark {
+
+  @CompilerControl(CompilerControl.Mode.DONT_INLINE)
+  def isAutoReceived(message: AnyRef): Boolean =
+    message.isInstanceOf[AutoReceivedMessage]
+
+  @CompilerControl(CompilerControl.Mode.DONT_INLINE)
+  def isNotInfluenceReceiveTimeout(message: AnyRef): Boolean =
+    message.isInstanceOf[NotInfluenceReceiveTimeout]
+
+  @CompilerControl(CompilerControl.Mode.DONT_INLINE)
+  def isNotInfluenceReceiveTimeoutGuarded(message: AnyRef): Boolean =
+    dungeon.ReceiveTimeoutCompat.isNotInfluenceReceiveTimeout(message)
+
+  @Benchmark
+  def typePolluted(input: ReceiveTimeoutTypePollutionInput): Int = {
+    val message = input.next()
+    val before = isNotInfluenceReceiveTimeout(message)
+    val auto = isAutoReceived(message)
+    val after = isNotInfluenceReceiveTimeout(message)
+    (if (before) 1 else 0) | (if (auto) 2 else 0) | (if (after) 4 else 0)
+  }
+
+  @Benchmark
+  def concreteTypeGuard(input: ReceiveTimeoutTypePollutionInput): Int = {
+    val message = input.next()
+    val before = isNotInfluenceReceiveTimeoutGuarded(message)
+    val auto = isAutoReceived(message)
+    val after = isNotInfluenceReceiveTimeoutGuarded(message)
+    (if (before) 1 else 0) | (if (auto) 2 else 0) | (if (after) 4 else 0)
+  }
+
+  @Benchmark
+  def concreteTypeGuardAndStateChange(input: 
ReceiveTimeoutTypePollutionInput): Int = {
+    val message = input.next()
+    val before = isNotInfluenceReceiveTimeoutGuarded(message)
+    val auto = isAutoReceived(message)
+    (if (before) 1 else 0) | (if (auto) 2 else 0)
+  }
+}


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

Reply via email to