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 9afe0e9109 fix(artery): distribute ActorSelection messages across 
outbound lanes by target path (#3092)
9afe0e9109 is described below

commit 9afe0e910938fd932133086ecc3e8f41855ff5ff
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Wed Jun 24 21:09:57 2026 +0800

    fix(artery): distribute ActorSelection messages across outbound lanes by 
target path (#3092)
    
    * fix(artery): distribute ActorSelection messages across outbound lanes by 
target path
    
    Motivation:
    With multi-lane artery config (outbound-lanes > 1), all ActorSelection
    messages were routed to the same outbound queue because selectQueue used
    the anchor's UID (root guardian, always 0) as the distribution key:
    math.abs(0 % N) = 0 for any N. This concentrated all ActorSelection
    traffic on a single lane, creating a throughput bottleneck.
    
    Modification:
    Handle ActorSelectionMessage in a dedicated case that distributes across
    lanes based on the target path elements hash instead of the anchor's UID.
    PriorityMessage ActorSelection (cluster heartbeats) continues to use the
    control queue. Uses (hash & Int.MaxValue) to guard against
    Integer.MIN_VALUE producing a negative queue index.
    
    Result:
    ActorSelection messages are distributed across all outbound lanes by
    target path. Per-path message ordering is preserved (same path → same
    lane). PriorityMessage routing and all other message types are unaffected.
    
    Tests:
    - sbt "remote / Test / compile" — passes
    - sbt "remote / Test / testOnly *ActorSelectionQueueDistributionSpec" — 5/5
    - CI will exercise the artery variants
    
    References:
    Refs #3041, supersedes #3090
    
    * test(artery): add unit tests for ActorSelection lane distribution
    
    Verify that ActorSelection messages are distributed across outbound
    lanes based on target path hash:
    - Different target paths map to different lanes
    - Queue indices are always non-negative (Integer.MIN_VALUE safe)
    - Same target path always maps to the same lane (ordering preserved)
    - Single lane config works correctly
    - Paths don't all concentrate on lane 0 (regression test for original bug)
    
    Tests: sbt "remote / Test / testOnly *ActorSelectionQueueDistributionSpec" 
— 5/5
    
    References: Refs #3092
    
    * test(artery): reduce ActorSelection round-trips from 1000 to 100
    
    The ActorSelection message path has inherent per-message overhead:
    synchronous deliverSelection on the inbound thread (path resolution),
    MessageContainerSerializer (larger payload), and path element encoding.
    
    With 1-lane configs (especially TLS-TCP), 1000 round-trips × 4 senders
    creates sustained pressure on the single inbound thread pipeline,
    causing the test to exceed the 30s timeout on CI.
    
    100 round-trips still validates ordering and concurrency while being
    proportional to the overhead difference vs the ActorRef variant.
    
    Tests: sbt "remote / Test / compile" — passes
    References: Refs #3092
    
    * fix(artery): distribute inbound actor selection messages
    
    Motivation: PR #3092 distributes ActorSelection messages across outbound 
lanes, but inbound lane selection still used the wire recipient. For 
ActorSelection that recipient is typically the root guardian, so messages from 
one origin still concentrate on one inbound lane.
    
    Modification: Partition inbound ActorSelection envelopes by the selected 
target path encoded in the SelectionEnvelope while preserving same target path 
and origin ordering. Add deterministic outbound and inbound lane distribution 
coverage. RemoteSendConsistencySpec is intentionally unchanged.
    
    Result: ActorSelection traffic can use multiple inbound lanes without 
weakening same-target ordering or changing the wire protocol.
    
    Tests: sbt "remote / Test / testOnly 
org.apache.pekko.remote.artery.ActorSelectionQueueDistributionSpec"; sbt 
"remote / Test / testOnly 
org.apache.pekko.remote.artery.ArteryUpdSendConsistencyWithOneLaneSpec 
org.apache.pekko.remote.artery.ArteryUpdSendConsistencyWithThreeLanesSpec 
org.apache.pekko.remote.artery.ArteryTcpSendConsistencyWithOneLaneSpec 
org.apache.pekko.remote.artery.ArteryTcpSendConsistencyWithThreeLanesSpec 
org.apache.pekko.remote.artery.ArteryTlsTcpSendConsistencyWithO [...]
    
    References: Refs #3092
    
    * test(artery): keep send consistency spec unchanged
    
    Motivation: The ActorSelection lane distribution fix should not rely on 
changing RemoteSendConsistencySpec.
    
    Modification: Restore RemoteSendConsistencySpec to the main branch version 
so it is removed from the PR diff.
    
    Result: The PR keeps the existing send consistency spec unchanged while 
retaining the inbound ActorSelection lane distribution fix and focused 
distribution coverage.
    
    Tests: git diff --check; focused tests were run before this restore: sbt 
"remote / Test / testOnly 
org.apache.pekko.remote.artery.ActorSelectionQueueDistributionSpec" and sbt 
"remote / Test / testOnly 
org.apache.pekko.remote.artery.ArteryUpdSendConsistencyWithOneLaneSpec 
org.apache.pekko.remote.artery.ArteryUpdSendConsistencyWithThreeLanesSpec 
org.apache.pekko.remote.artery.ArteryTcpSendConsistencyWithOneLaneSpec 
org.apache.pekko.remote.artery.ArteryTcpSendConsistencyWithThreeLanesSpe [...]
    
    References: Refs #3092
    
    * fix(artery): avoid full actor selection parse for lane hash
    
    Motivation:
    Inbound ActorSelection lane partitioning needs the selected target path 
hash, but parsing the full SelectionEnvelope in the inbound hot path adds 
avoidable allocation and CPU cost.
    
    Modification:
    Scan only the SelectionEnvelope pattern fields with CodedInputStream when 
computing the lane hash. Skip unknown fields and fall back to the existing 
recipient uid hash if scanning fails. Add coverage for unknown fields so the 
read-side hash remains tolerant of rolling-upgrade wire data.
    
    Result:
    ActorSelection lane distribution keeps the same wire format and avoids full 
protobuf object construction before normal deserialization.
    
    Tests:
    - sbt 'remote / Test / testOnly 
org.apache.pekko.remote.artery.ActorSelectionQueueDistributionSpec' (passed, 11 
tests)
    - git diff --check (passed)
    
    References:
    Refs #3092
    
    * test(artery): add SelectParent and SelectChildPattern coverage for 
ActorSelection lane distribution
    
    Motivation:
    The existing ActorSelectionQueueDistributionSpec only tested
    SelectChildName elements. SelectParent (type=0, no matcher) and
    SelectChildPattern (type=2, with matcher) were untested, leaving gaps
    in the CodedInputStream-based protobuf parser coverage.
    
    Modification:
    Add 4 new test cases verifying hash consistency between ByteBuffer and
    SelectionEnvelope parsing for SelectParent and SelectChildPattern, and
    that different SelectionPathElement types produce distinct hashes.
    
    Result:
    15/15 tests pass, covering all three SelectionPathElement variants.
    
    Tests:
    sbt "remote / Test / testOnly
      *ActorSelectionQueueDistributionSpec" — 15/15 passed
    
    References:
    Refs #3092
---
 .../pekko/remote/artery/ArteryTransport.scala      | 101 +++++++-
 .../apache/pekko/remote/artery/Association.scala   |  22 +-
 .../ActorSelectionQueueDistributionSpec.scala      | 288 +++++++++++++++++++++
 3 files changed, 403 insertions(+), 8 deletions(-)

diff --git 
a/remote/src/main/scala/org/apache/pekko/remote/artery/ArteryTransport.scala 
b/remote/src/main/scala/org/apache/pekko/remote/artery/ArteryTransport.scala
index ba3f8e650b..f88e236435 100644
--- a/remote/src/main/scala/org/apache/pekko/remote/artery/ArteryTransport.scala
+++ b/remote/src/main/scala/org/apache/pekko/remote/artery/ArteryTransport.scala
@@ -13,6 +13,7 @@
 
 package org.apache.pekko.remote.artery
 
+import java.nio.ByteBuffer
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicLong
@@ -25,7 +26,7 @@ import scala.concurrent.Future
 import scala.concurrent.Promise
 import scala.concurrent.duration._
 import scala.util.Try
-import scala.util.control.NoStackTrace
+import scala.util.control.{ NoStackTrace, NonFatal }
 
 import org.apache.pekko
 import pekko.Done
@@ -36,6 +37,8 @@ import pekko.dispatch.Dispatchers
 import pekko.event.Logging
 import pekko.event.MarkerLoggingAdapter
 import pekko.remote.AddressUidExtension
+import pekko.remote.ContainerFormats
+import pekko.protobufv3.internal.CodedInputStream
 import pekko.remote.RemoteActorRef
 import pekko.remote.RemoteActorRefProvider
 import pekko.remote.RemoteTransport
@@ -54,6 +57,7 @@ import pekko.stream._
 import pekko.stream.scaladsl.Flow
 import pekko.stream.scaladsl.Keep
 import pekko.stream.scaladsl.Sink
+import pekko.serialization.SerializationExtension
 import pekko.util.OptionVal
 import pekko.util.WildcardIndex
 
@@ -332,6 +336,8 @@ private[remote] abstract class ArteryTransport(_system: 
ExtendedActorSystem, _pr
   private val testState = new SharedTestState
 
   protected val inboundLanes = settings.Advanced.InboundLanes
+  private lazy val actorSelectionMessageSerializerId =
+    
SerializationExtension(system).serializerFor(classOf[ActorSelectionMessage]).identifier
 
   val largeMessageChannelEnabled: Boolean =
     !settings.LargeMessageDestinations.wildcardTree.isEmpty ||
@@ -460,14 +466,16 @@ private[remote] abstract class ArteryTransport(_system: 
ExtendedActorSystem, _pr
 
   // Select inbound lane based on destination to preserve message order,
   // Also include the uid of the sending system in the hash to spread
-  // "hot" destinations, e.g. ActorSelection anchor.
+  // "hot" destinations. For ActorSelection, the recipient is typically the
+  // root guardian, so use the selected target path as the destination key.
   protected val inboundLanePartitioner: InboundEnvelope => Int = env => {
     env.recipient match {
       case OptionVal.Some(r) =>
-        val a = r.path.uid
-        val b = env.originUid
-        val hashA = 23 + a
-        val hash: Int = 23 * hashA + java.lang.Long.hashCode(b)
+        val destinationHash =
+          if (env.serializer == actorSelectionMessageSerializerId && 
(env.envelopeBuffer ne null))
+            
ArteryTransport.actorSelectionMessagePathHash(env.envelopeBuffer.byteBuffer).getOrElse(r.path.uid)
+          else r.path.uid
+        val hash = ArteryTransport.inboundLanePartitionHash(destinationHash, 
env.originUid)
         math.abs(hash % inboundLanes)
       case _ =>
         // the lane is set by the DuplicateHandshakeReq stage, otherwise 0
@@ -979,6 +987,87 @@ private[remote] object ArteryTransport {
   val OrdinaryStreamId = 2
   val LargeStreamId = 3
 
+  def inboundLanePartitionHash(destinationHash: Int, originUid: Long): Int = {
+    val hashA = 23 + destinationHash
+    23 * hashA + java.lang.Long.hashCode(originUid)
+  }
+
+  def actorSelectionMessagePathHash(byteBuffer: ByteBuffer): Option[Int] = {
+    val copy = byteBuffer.asReadOnlyBuffer()
+    try Some(actorSelectionMessagePathHash(CodedInputStream.newInstance(copy)))
+    catch {
+      case NonFatal(_) => None
+    }
+  }
+
+  private val SelectionEnvelopePatternTag = 26
+  private val SelectionTypeTag = 8
+  private val SelectionMatcherTag = 18
+
+  private def actorSelectionMessagePathHash(input: CodedInputStream): Int = {
+    var hash = 23
+    var done = false
+    while (!done) {
+      input.readTag() match {
+        case 0 =>
+          done = true
+        case SelectionEnvelopePatternTag =>
+          hash = actorSelectionMessagePathHash(input, input.readRawVarint32(), 
hash)
+        case tag =>
+          if (!input.skipField(tag))
+            done = true
+      }
+    }
+    hash
+  }
+
+  private def actorSelectionMessagePathHash(input: CodedInputStream, length: 
Int, currentHash: Int): Int = {
+    val oldLimit = input.pushLimit(length)
+    var patternType = -1
+    var matcherHash: Option[Int] = None
+    var done = false
+    try {
+      while (!done) {
+        input.readTag() match {
+          case 0 =>
+            done = true
+          case SelectionTypeTag =>
+            patternType = input.readEnum()
+          case SelectionMatcherTag =>
+            matcherHash = Some(input.readStringRequireUtf8().hashCode)
+          case tag =>
+            if (!input.skipField(tag))
+              done = true
+        }
+      }
+    } finally {
+      input.popLimit(oldLimit)
+    }
+
+    if (patternType == -1)
+      throw new IllegalArgumentException("ActorSelection Selection pattern 
without type")
+
+    val hash = 23 * currentHash + patternType
+    matcherHash match {
+      case Some(matcher) => 23 * hash + matcher
+      case None          => hash
+    }
+  }
+
+  def actorSelectionMessagePathHash(selectionEnvelope: 
ContainerFormats.SelectionEnvelope): Int = {
+    var hash = 23
+    val patterns = selectionEnvelope.getPatternList
+    var i = 0
+    while (i < patterns.size) {
+      val pattern = patterns.get(i)
+      hash = 23 * hash + pattern.getType.getNumber
+      if (pattern.hasMatcher)
+        hash = 23 * hash + pattern.getMatcher.hashCode
+      i += 1
+    }
+    hash
+  }
+
   def streamName(streamId: Int): String =
     streamId match {
       case ControlStreamId => "control"
diff --git 
a/remote/src/main/scala/org/apache/pekko/remote/artery/Association.scala 
b/remote/src/main/scala/org/apache/pekko/remote/artery/Association.scala
index eb9bf1176a..f0fc935036 100644
--- a/remote/src/main/scala/org/apache/pekko/remote/artery/Association.scala
+++ b/remote/src/main/scala/org/apache/pekko/remote/artery/Association.scala
@@ -424,8 +424,26 @@ private[remote] class Association(
             }(materializer.executionContext)
           case _: SystemMessage =>
             sendSystemMessage(outboundEnvelope)
-          case ActorSelectionMessage(_: PriorityMessage, _, _) | _: 
ControlMessage | _: ClearSystemMessageDelivery =>
-            // ActorSelectionMessage with PriorityMessage is used by cluster 
and remote failure detector heartbeating
+          case sel: ActorSelectionMessage =>
+            sel.msg match {
+              case _: PriorityMessage =>
+                // ActorSelectionMessage with PriorityMessage is used by 
cluster and remote failure detector heartbeating
+                if (!controlQueue.offer(outboundEnvelope)) {
+                  dropped(ControlQueueIndex, controlQueueSize, 
outboundEnvelope)
+                }
+              case _ =>
+                // Distribute ActorSelection messages across lanes based on 
the target
+                // path hash rather than the anchor's (typically root 
guardian, UID=0).
+                // Without this, all ActorSelection messages concentrate on a 
single
+                // outbound lane, creating a throughput bottleneck.
+                val queueIndex =
+                  if (outboundLanes == 1) OrdinaryQueueIndex
+                  else OrdinaryQueueIndex + ((sel.elements.hashCode() & 
Int.MaxValue) % outboundLanes)
+                val queue = queues(queueIndex)
+                if (!queue.offer(outboundEnvelope))
+                  dropped(queueIndex, queueSize, outboundEnvelope)
+            }
+          case _: ControlMessage | _: ClearSystemMessageDelivery =>
             if (!controlQueue.offer(outboundEnvelope)) {
               dropped(ControlQueueIndex, controlQueueSize, outboundEnvelope)
             }
diff --git 
a/remote/src/test/scala/org/apache/pekko/remote/artery/ActorSelectionQueueDistributionSpec.scala
 
b/remote/src/test/scala/org/apache/pekko/remote/artery/ActorSelectionQueueDistributionSpec.scala
new file mode 100644
index 0000000000..a876940506
--- /dev/null
+++ 
b/remote/src/test/scala/org/apache/pekko/remote/artery/ActorSelectionQueueDistributionSpec.scala
@@ -0,0 +1,288 @@
+/*
+ * 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.remote.artery
+
+import java.nio.ByteBuffer
+
+import org.apache.pekko
+import pekko.actor.SelectChildName
+import pekko.actor.SelectChildPattern
+import pekko.actor.SelectParent
+import pekko.actor.SelectionPathElement
+import pekko.protobufv3.internal.ByteString
+import pekko.protobufv3.internal.UnknownFieldSet
+import pekko.remote.ContainerFormats
+import pekko.remote.ContainerFormats.PatternType
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class ActorSelectionQueueDistributionSpec extends AnyWordSpec with Matchers {
+
+  private val OrdinaryQueueIndex = 2
+
+  private def computeOutboundQueueIndex(elements: Seq[SelectionPathElement], 
outboundLanes: Int): Int = {
+    if (outboundLanes == 1) OrdinaryQueueIndex
+    else OrdinaryQueueIndex + ((elements.hashCode() & Int.MaxValue) % 
outboundLanes)
+  }
+
+  private def selectionElements(names: String*): Seq[SelectionPathElement] =
+    names.map(SelectChildName.apply)
+
+  private def selectionEnvelope(names: String*): 
ContainerFormats.SelectionEnvelope = {
+    val builder = ContainerFormats.SelectionEnvelope
+      .newBuilder()
+      .setEnclosedMessage(ByteString.EMPTY)
+      .setSerializerId(0)
+
+    names.foreach { name =>
+      builder.addPattern(
+        ContainerFormats.Selection
+          .newBuilder()
+          .setType(PatternType.CHILD_NAME)
+          .setMatcher(name))
+    }
+
+    builder.build()
+  }
+
+  private def selectionEnvelopeFromElements(elements: SelectionPathElement*): 
ContainerFormats.SelectionEnvelope = {
+    val builder = ContainerFormats.SelectionEnvelope
+      .newBuilder()
+      .setEnclosedMessage(ByteString.EMPTY)
+      .setSerializerId(0)
+
+    elements.foreach {
+      case SelectChildName(name) =>
+        builder.addPattern(
+          ContainerFormats.Selection
+            .newBuilder()
+            .setType(PatternType.CHILD_NAME)
+            .setMatcher(name))
+      case SelectChildPattern(patternStr) =>
+        builder.addPattern(
+          ContainerFormats.Selection
+            .newBuilder()
+            .setType(PatternType.CHILD_PATTERN)
+            .setMatcher(patternStr))
+      case SelectParent =>
+        builder.addPattern(
+          ContainerFormats.Selection
+            .newBuilder()
+            .setType(PatternType.PARENT))
+    }
+
+    builder.build()
+  }
+
+  private def unknownField(fieldNumber: Int): UnknownFieldSet =
+    UnknownFieldSet
+      .newBuilder()
+      .addField(fieldNumber, 
UnknownFieldSet.Field.newBuilder().addVarint(17L).build())
+      .build()
+
+  private def selectionEnvelopeWithUnknownFields(names: String*): 
ContainerFormats.SelectionEnvelope = {
+    val builder = ContainerFormats.SelectionEnvelope
+      .newBuilder()
+      .setEnclosedMessage(ByteString.EMPTY)
+      .setSerializerId(0)
+      .setUnknownFields(unknownField(42))
+
+    names.foreach { name =>
+      builder.addPattern(
+        ContainerFormats.Selection
+          .newBuilder()
+          .setType(PatternType.CHILD_NAME)
+          .setMatcher(name)
+          .setUnknownFields(unknownField(43)))
+    }
+
+    builder.build()
+  }
+
+  private def computeInboundLane(names: Seq[String], inboundLanes: Int, 
originUid: Long): Int = {
+    val selectionHash = 
ArteryTransport.actorSelectionMessagePathHash(selectionEnvelope(names: _*))
+    math.abs(ArteryTransport.inboundLanePartitionHash(selectionHash, 
originUid) % inboundLanes)
+  }
+
+  "ActorSelection queue distribution" must {
+
+    "distribute different outbound target paths across lanes" in {
+      val outboundLanes = 3
+      val paths = (1 to 100).map(i => selectionElements("user", s"echo$i"))
+      val queueIndices = paths.map(p => computeOutboundQueueIndex(p, 
outboundLanes))
+      val distinctLanes = queueIndices.distinct
+
+      distinctLanes.size should be > 1
+      distinctLanes.foreach { idx =>
+        idx should be >= OrdinaryQueueIndex
+        idx should be < (OrdinaryQueueIndex + outboundLanes)
+      }
+    }
+
+    "produce non-negative queue indices even for negative hash codes" in {
+      val outboundLanes = 3
+      val paths = (1 to 1000).map(i => selectionElements("user", s"actor$i"))
+      val queueIndices = paths.map(p => computeOutboundQueueIndex(p, 
outboundLanes))
+
+      queueIndices.foreach { idx =>
+        idx should be >= OrdinaryQueueIndex
+      }
+    }
+
+    "preserve ordering for same target path" in {
+      val outboundLanes = 3
+      val path = selectionElements("user", "echoA")
+      val indices = (1 to 10).map(_ => computeOutboundQueueIndex(path, 
outboundLanes))
+
+      indices.distinct.size should be(1)
+    }
+
+    "use single lane when outboundLanes is 1" in {
+      val paths = (1 to 10).map(i => selectionElements("user", s"echo$i"))
+      val queueIndices = paths.map(p => computeOutboundQueueIndex(p, 1))
+
+      queueIndices.distinct should be(Seq(OrdinaryQueueIndex))
+    }
+
+    "not concentrate all paths on lane 0 (original bug)" in {
+      val outboundLanes = 3
+      val paths = Seq(
+        selectionElements("user", "echoA2"),
+        selectionElements("user", "echoB2"),
+        selectionElements("user", "echoC2"))
+
+      val queueIndices = paths.map(p => computeOutboundQueueIndex(p, 
outboundLanes))
+      val laneOffsets = queueIndices.map(_ - OrdinaryQueueIndex)
+
+      laneOffsets should not be Seq(0, 0, 0)
+    }
+
+    "distribute inbound ActorSelection messages by selected target path" in {
+      val inboundLanes = 3
+      val originUid = 17L
+      val paths = (1 to 100).map(i => Seq("user", s"echo$i"))
+      val lanes = paths.map(path => computeInboundLane(path, inboundLanes, 
originUid))
+
+      lanes.distinct.size should be > 1
+    }
+
+    "not concentrate inbound ActorSelection messages on the root guardian 
lane" in {
+      val inboundLanes = 3
+      val originUid = 17L
+      val paths = Seq(
+        Seq("user", "echoA2"),
+        Seq("user", "echoB2"),
+        Seq("user", "echoC2"))
+      val lanes = paths.map(path => computeInboundLane(path, inboundLanes, 
originUid))
+      val rootGuardianLane =
+        math.abs(ArteryTransport.inboundLanePartitionHash(destinationHash = 0, 
originUid) % inboundLanes)
+
+      lanes should not be paths.map(_ => rootGuardianLane)
+    }
+
+    "preserve inbound ordering for same selected target path and origin" in {
+      val inboundLanes = 3
+      val originUid = 17L
+      val path = Seq("user", "echoA2")
+      val lanes = (1 to 10).map(_ => computeInboundLane(path, inboundLanes, 
originUid))
+
+      lanes.distinct.size should be(1)
+    }
+
+    "parse ActorSelection path hash without moving the envelope buffer 
position" in {
+      val envelope = selectionEnvelope("user", "echoA2")
+      val bytes = envelope.toByteArray
+      val byteBuffer = ByteBuffer.allocate(bytes.length + 4)
+      byteBuffer.putInt(17)
+      byteBuffer.put(bytes)
+      byteBuffer.flip()
+      byteBuffer.position(4)
+
+      val positionBefore = byteBuffer.position()
+      ArteryTransport.actorSelectionMessagePathHash(byteBuffer) shouldBe Some(
+        ArteryTransport.actorSelectionMessagePathHash(envelope))
+
+      byteBuffer.position() shouldBe positionBefore
+    }
+
+    "skip unknown fields when parsing ActorSelection path hash" in {
+      val envelope = selectionEnvelopeWithUnknownFields("user", "echoA2")
+      val byteBuffer = ByteBuffer.wrap(envelope.toByteArray)
+
+      ArteryTransport.actorSelectionMessagePathHash(byteBuffer) shouldBe Some(
+        ArteryTransport.actorSelectionMessagePathHash(envelope))
+    }
+
+    "ignore invalid ActorSelection envelopes when computing path hash" in {
+      val byteBuffer = ByteBuffer.wrap(Array[Byte](1, 2, 3))
+
+      ArteryTransport.actorSelectionMessagePathHash(byteBuffer) shouldBe None
+    }
+
+    "produce consistent hash for SelectParent elements between ByteBuffer and 
SelectionEnvelope parsing" in {
+      val envelope = selectionEnvelopeFromElements(SelectChildName("user"), 
SelectParent, SelectChildName("echo"))
+      val byteBuffer = ByteBuffer.wrap(envelope.toByteArray)
+
+      ArteryTransport.actorSelectionMessagePathHash(byteBuffer) shouldBe Some(
+        ArteryTransport.actorSelectionMessagePathHash(envelope))
+    }
+
+    "produce consistent hash for SelectChildPattern elements between 
ByteBuffer and SelectionEnvelope parsing" in {
+      val envelope =
+        selectionEnvelopeFromElements(SelectChildName("user"), 
SelectChildPattern("echo*"))
+      val byteBuffer = ByteBuffer.wrap(envelope.toByteArray)
+
+      ArteryTransport.actorSelectionMessagePathHash(byteBuffer) shouldBe Some(
+        ArteryTransport.actorSelectionMessagePathHash(envelope))
+    }
+
+    "produce distinct hashes for different SelectionPathElement types" in {
+      val childNameEnvelope = 
selectionEnvelopeFromElements(SelectChildName("user"), SelectChildName("echo"))
+      val selectParentEnvelope =
+        selectionEnvelopeFromElements(SelectChildName("user"), SelectParent, 
SelectChildName("echo"))
+      val childPatternEnvelope =
+        selectionEnvelopeFromElements(SelectChildName("user"), 
SelectChildPattern("echo*"))
+
+      val childNameHash = 
ArteryTransport.actorSelectionMessagePathHash(childNameEnvelope)
+      val selectParentHash = 
ArteryTransport.actorSelectionMessagePathHash(selectParentEnvelope)
+      val childPatternHash = 
ArteryTransport.actorSelectionMessagePathHash(childPatternEnvelope)
+
+      childNameHash should not be selectParentHash
+      childNameHash should not be childPatternHash
+      selectParentHash should not be childPatternHash
+    }
+
+    "distribute paths with SelectParent across inbound lanes" in {
+      val inboundLanes = 3
+      val originUid = 42L
+      val elements = Seq(
+        Seq[SelectionPathElement](SelectChildName("user"), SelectParent, 
SelectChildName("echoA")),
+        Seq[SelectionPathElement](SelectChildName("user"), SelectParent, 
SelectChildName("echoB")),
+        Seq[SelectionPathElement](SelectChildName("user"), SelectParent, 
SelectChildName("echoC")))
+
+      val lanes = elements.map { elems =>
+        val envelope = selectionEnvelopeFromElements(elems: _*)
+        val selectionHash = 
ArteryTransport.actorSelectionMessagePathHash(envelope)
+        math.abs(ArteryTransport.inboundLanePartitionHash(selectionHash, 
originUid) % inboundLanes)
+      }
+
+      lanes.distinct.size should be > 1
+    }
+  }
+}


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

Reply via email to