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 965b6b85c6 fix: ignore stale classic remoting reader ACKs (#3173)
965b6b85c6 is described below

commit 965b6b85c6a6ee24e0e7cf34b3063292d9035fa6
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sat Jun 27 21:51:32 2026 +0800

    fix: ignore stale classic remoting reader ACKs (#3173)
    
    Motivation:
    Classic remoting ACKs forwarded by EndpointReader were not tied to the 
reader UID, so a delayed ACK from an old reader could affect the resend buffer 
after a new UID was confirmed.
    
    Modification:
    Tag decoded ACKs with the EndpointReader UID and have 
ReliableDeliverySupervisor only process ACKs from the currently confirmed UID. 
Add a supervisor-level regression test covering stale and current reader ACKs.
    
    Result:
    Stale reader ACKs are ignored while current-reader ACKs still acknowledge 
the active resend buffer.
    
    Tests:
    - sbt "remote / Test / testOnly 
org.apache.pekko.remote.ReliableDeliverySupervisorSpec"
    - sbt headerCreateAll
    - sbt +headerCheckAll
    - scalafmt --mode diff-ref=origin/main
    - scalafmt --list --mode diff-ref=origin/main
    - git diff --check
    - qodercli -p --output-format stream-json --attachment 
/tmp/pekko-3160-qoder-review.diff (No must-fix findings)
    - sbt sortImports (failed: scalafix/scala.meta NoSuchMethodError in 
stream-tests and multi-node-testkit)
    
    References:
    Fixes #3160
---
 .../scala/org/apache/pekko/remote/Endpoint.scala   |  65 +++++----
 .../remote/ReliableDeliverySupervisorSpec.scala    | 152 +++++++++++++++++++++
 2 files changed, 188 insertions(+), 29 deletions(-)

diff --git a/remote/src/main/scala/org/apache/pekko/remote/Endpoint.scala 
b/remote/src/main/scala/org/apache/pekko/remote/Endpoint.scala
index aab9bef792..9dfdf249eb 100644
--- a/remote/src/main/scala/org/apache/pekko/remote/Endpoint.scala
+++ b/remote/src/main/scala/org/apache/pekko/remote/Endpoint.scala
@@ -226,6 +226,8 @@ private[remote] object ReliableDeliverySupervisor {
   case object Ungate
   case object AttemptSysMsgRedelivery
   final case class GotUid(uid: Int, remoteAddres: Address)
+  @nowarn("msg=deprecated")
+  final case class AckFromReader(readerUid: Int, ack: Ack) extends 
NoSerializationVerificationNeeded
 
   case object IsIdle
   case object Idle
@@ -363,33 +365,9 @@ private[remote] class ReliableDeliverySupervisor(
     case IsIdle  => // Do not reply, we will Terminate soon, or send a GotUid
     case s: Send =>
       handleSend(s)
-    case ack: Ack =>
-      // If we are not sure about the UID just ignore the ack. Ignoring is 
fine.
-      if (uidConfirmed) {
-        if (ack.cumulativeAck > resendBuffer.maxSeq)
-          log.warning(
-            "Ignoring stale system message acknowledgement from remote system 
[{}]. Highest SEQ so far was [{}] but cumulative ACK is [{}].",
-            remoteAddress,
-            resendBuffer.maxSeq,
-            ack.cumulativeAck)
-        else {
-          try resendBuffer = resendBuffer.acknowledge(ack)
-          catch {
-            case NonFatal(e) =>
-              throw new HopelessAssociation(
-                localAddress,
-                remoteAddress,
-                uid,
-                new IllegalStateException(
-                  s"Error encountered while processing system message " +
-                  s"acknowledgement buffer: $resendBuffer ack: $ack",
-                  e))
-          }
-
-          resendNacked()
-        }
-      }
-    case AttemptSysMsgRedelivery =>
+    case AckFromReader(readerUid, ack) => handleAck(readerUid, ack)
+    case _: Ack                        => () // Ignore untagged ACKs. 
EndpointReader attaches its UID before forwarding ACKs.
+    case AttemptSysMsgRedelivery       =>
       if (uidConfirmed) resendAll()
     case Terminated(_) =>
       currentHandle = None
@@ -525,6 +503,33 @@ private[remote] class ReliableDeliverySupervisor(
     resendBuffer.nonAcked.take(settings.SysResendLimit).foreach { writer ! _ }
   }
 
+  private def handleAck(readerUid: Int, ack: Ack): Unit =
+    // If we are not sure about the UID just ignore the ack. Ignoring is fine.
+    if (uidConfirmed && uid.contains(readerUid)) {
+      if (ack.cumulativeAck > resendBuffer.maxSeq)
+        log.warning(
+          "Ignoring stale system message acknowledgement from remote system 
[{}]. Highest SEQ so far was [{}] but cumulative ACK is [{}].",
+          remoteAddress,
+          resendBuffer.maxSeq,
+          ack.cumulativeAck)
+      else {
+        try resendBuffer = resendBuffer.acknowledge(ack)
+        catch {
+          case NonFatal(e) =>
+            throw new HopelessAssociation(
+              localAddress,
+              remoteAddress,
+              uid,
+              new IllegalStateException(
+                s"Error encountered while processing system message " +
+                s"acknowledgement buffer: $resendBuffer ack: $ack",
+                e))
+        }
+
+        resendNacked()
+      }
+    }
+
   private def tryBuffer(s: Send): Unit =
     try {
       resendBuffer = resendBuffer.buffer(s)
@@ -1152,7 +1157,8 @@ private[remote] class EndpointReader(
     case InboundPayload(p) if p.length <= transport.maximumPayloadBytes =>
       val (ackOption, msgOption) = tryDecodeMessageAndAck(p)
 
-      for (ack <- ackOption; reliableDelivery <- reliableDeliverySupervisor) 
reliableDelivery ! ack
+      for (ack <- ackOption; reliableDelivery <- reliableDeliverySupervisor)
+        reliableDelivery ! ReliableDeliverySupervisor.AckFromReader(uid, ack)
 
       msgOption match {
         case Some(msg) =>
@@ -1200,7 +1206,8 @@ private[remote] class EndpointReader(
 
     case InboundPayload(p) if p.length <= transport.maximumPayloadBytes =>
       val (ackOption, msgOption) = tryDecodeMessageAndAck(p)
-      for (ack <- ackOption; reliableDelivery <- reliableDeliverySupervisor) 
reliableDelivery ! ack
+      for (ack <- ackOption; reliableDelivery <- reliableDeliverySupervisor)
+        reliableDelivery ! ReliableDeliverySupervisor.AckFromReader(uid, ack)
 
       if (log.isWarningEnabled)
         log.warning(
diff --git 
a/remote/src/test/scala/org/apache/pekko/remote/ReliableDeliverySupervisorSpec.scala
 
b/remote/src/test/scala/org/apache/pekko/remote/ReliableDeliverySupervisorSpec.scala
new file mode 100644
index 0000000000..d433f03bc0
--- /dev/null
+++ 
b/remote/src/test/scala/org/apache/pekko/remote/ReliableDeliverySupervisorSpec.scala
@@ -0,0 +1,152 @@
+/*
+ * 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
+
+import java.util.concurrent.ConcurrentHashMap
+
+import scala.annotation.nowarn
+import scala.concurrent.Promise
+
+import org.apache.pekko
+import pekko.actor.{ ActorRef, Address, Nobody, RootActorPath }
+import pekko.remote.EndpointManager.{ Link, ResendState, Send }
+import pekko.remote.ReliableDeliverySupervisor.AckFromReader
+import pekko.remote.transport._
+import pekko.testkit.{ ImplicitSender, PekkoSpec, TestActorRef, TestProbe }
+import pekko.util.OptionVal
+
+import com.typesafe.config.ConfigFactory
+
+object ReliableDeliverySupervisorSpec {
+  val config = ConfigFactory.parseString("""
+    pekko {
+      actor.provider = remote
+      actor.allow-java-serialization = on
+      actor.warn-about-java-serializer-usage = off
+
+      remote {
+        artery.enabled = off
+        warn-about-direct-use = off
+        use-unsafe-remote-features-outside-cluster = on
+
+        classic {
+          resend-interval = 30 s
+          system-message-buffer-size = 10
+          system-message-ack-piggyback-timeout = 30 s
+
+          netty.tcp {
+            hostname = "localhost"
+            port = 0
+          }
+        }
+      }
+    }
+  """)
+}
+
+@nowarn("msg=deprecated")
+class ReliableDeliverySupervisorSpec extends 
PekkoSpec(ReliableDeliverySupervisorSpec.config) with ImplicitSender {
+
+  private val localAddress = Address("pekko.test", system.name, "localhost", 
2551)
+  private val remoteAddress = Address("pekko.test", "remote-system", 
"localhost", 2552)
+  private val oldUid = 1
+  private val newUid = 2
+
+  "ReliableDeliverySupervisor" must {
+    "ignore ACKs from stale EndpointReader UIDs after a new UID is confirmed" 
in {
+      val parentProbe = TestProbe()
+      val supervisor = newSupervisor(initialUid = oldUid, parentProbe.ref)
+      val underlying = supervisor.underlyingActor
+
+      supervisor ! ReliableDeliverySupervisor.GotUid(newUid, remoteAddress)
+      parentProbe.expectMsg(ReliableDeliverySupervisor.GotUid(newUid, 
remoteAddress))
+      underlying.resendBuffer = bufferWith(0L, 1L)
+
+      supervisor ! AckFromReader(oldUid, Ack(SeqNo(0)))
+
+      underlying.resendBuffer.nonAcked.map(_.seq) should ===(Vector(SeqNo(0), 
SeqNo(1)))
+
+      supervisor ! Ack(SeqNo(0))
+
+      underlying.resendBuffer.nonAcked.map(_.seq) should ===(Vector(SeqNo(0), 
SeqNo(1)))
+
+      supervisor ! AckFromReader(newUid, Ack(SeqNo(0)))
+
+      underlying.resendBuffer.nonAcked.map(_.seq) should ===(Vector(SeqNo(1)))
+      underlying.resendBuffer = new AckedSendBuffer[Send](0)
+    }
+  }
+
+  private def newSupervisor(initialUid: Int, parent: ActorRef): 
TestActorRef[ReliableDeliverySupervisor] = {
+    val registry = new TestTransport.AssociationRegistry
+    val underlyingTransport =
+      new TestTransport(localAddress.copy(protocol = "test"), registry, 
schemeIdentifier = "test")
+    underlyingTransport.writeBehavior.pushConstant(true)
+    val protocolTransport = new PekkoProtocolTransport(
+      underlyingTransport,
+      system,
+      new PekkoProtocolSettings(system.settings.config),
+      PekkoPduProtobufCodec)
+    val underlyingHandle =
+      TestAssociationHandle(localAddress.copy(protocol = "test"), 
remoteAddress.copy(protocol = "test"),
+        underlyingTransport, inbound = true)
+    val handle = new PekkoProtocolHandle(
+      localAddress,
+      remoteAddress,
+      Promise[AssociationHandle.HandleEventListener](),
+      underlyingHandle,
+      HandshakeInfo(remoteAddress, initialUid),
+      TestProbe().ref,
+      PekkoPduProtobufCodec,
+      RARP(system).provider.remoteSettings.ProtocolName)
+
+    TestActorRef[ReliableDeliverySupervisor](
+      ReliableDeliverySupervisor.props(
+        handleOrActive = Some(handle),
+        localAddress,
+        remoteAddress,
+        refuseUid = None,
+        protocolTransport,
+        RARP(system).provider.remoteSettings,
+        PekkoPduProtobufCodec,
+        new ConcurrentHashMap[Link, ResendState]),
+      parent)
+  }
+
+  private def bufferWith(seqNumbers: Long*): AckedSendBuffer[Send] =
+    seqNumbers.foldLeft(new AckedSendBuffer[Send](10)) { (buffer, seq) =>
+      buffer.buffer(send(seq))
+    }
+
+  private def send(seq: Long): Send =
+    Send(
+      message = s"msg-$seq",
+      senderOption = OptionVal.None,
+      recipient = remoteRef,
+      seqOpt = Some(SeqNo(seq)))
+
+  private def remoteRef: RemoteActorRef =
+    new RemoteActorRef(
+      RARP(system).provider.transport,
+      localAddress,
+      RootActorPath(remoteAddress) / "user" / "recipient",
+      Nobody,
+      props = None,
+      deploy = None,
+      acceptProtocolNames = Set("pekko"))
+}


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

Reply via email to