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 07a346cc4b fix: validate full UniqueAddress in Artery system message 
ACK/NACK handling (#3175)
07a346cc4b is described below

commit 07a346cc4bb7277cde2256426b6bb012664be4cf
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Thu Jun 25 04:34:45 2026 +0800

    fix: validate full UniqueAddress in Artery system message ACK/NACK handling 
(#3175)
    
    Motivation:
    SystemMessageDelivery.notify() only compared the Address portion of
    incoming ACK/NACK messages, ignoring the UID. When a remote node
    restarts and obtains a new UID, a delayed ACK from the previous
    incarnation (same address, different UID) could pass the check and
    incorrectly clear the unacknowledged buffer, causing system messages
    to be silently dropped.
    
    Modification:
    Add isFromCurrentRemote() helper that checks the full UniqueAddress
    (address + UID) against the association's known uniqueRemoteAddress.
    Falls back to address-only matching when uniqueRemoteAddress is not
    yet established (pre-handshake), preserving existing behavior.
    
    Result:
    Stale ACKs from previous remote incarnations are rejected, preventing
    incorrect removal of system messages from the resend buffer.
    
    Tests:
    - sbt "remote / Test / testOnly 
org.apache.pekko.remote.artery.SystemMessageDeliverySpec" — all 9 tests pass
    
    References:
    Fixes #3174, Refs #3160
---
 .../remote/artery/SystemMessageDelivery.scala      | 10 ++++-
 .../remote/artery/SystemMessageDeliverySpec.scala  | 44 ++++++++++++++++++++++
 2 files changed, 52 insertions(+), 2 deletions(-)

diff --git 
a/remote/src/main/scala/org/apache/pekko/remote/artery/SystemMessageDelivery.scala
 
b/remote/src/main/scala/org/apache/pekko/remote/artery/SystemMessageDelivery.scala
index e9a59b743d..f1cdf18559 100644
--- 
a/remote/src/main/scala/org/apache/pekko/remote/artery/SystemMessageDelivery.scala
+++ 
b/remote/src/main/scala/org/apache/pekko/remote/artery/SystemMessageDelivery.scala
@@ -111,6 +111,12 @@ import pekko.util.PrettyDuration.PrettyPrintableDuration
       private def remoteAddressLogParam: String =
         
outboundContext.associationState.uniqueRemoteAddress().getOrElse(remoteAddress).toString
 
+      private def isFromCurrentRemote(from: UniqueAddress): Boolean =
+        outboundContext.associationState.uniqueRemoteAddress() match {
+          case Some(uniqueRemote) => uniqueRemote == from
+          case None               => from.address == remoteAddress
+        }
+
       override protected def logSource: Class[?] = 
classOf[SystemMessageDelivery]
 
       override def preStart(): Unit = {
@@ -156,8 +162,8 @@ import pekko.util.PrettyDuration.PrettyPrintableDuration
       // ControlMessageObserver, external call
       override def notify(inboundEnvelope: InboundEnvelope): Unit = {
         inboundEnvelope.message match {
-          case ack: Ack   => if (ack.from.address == remoteAddress) 
ackCallback.invoke(ack)
-          case nack: Nack => if (nack.from.address == remoteAddress) 
nackCallback.invoke(nack)
+          case ack: Ack   => if (isFromCurrentRemote(ack.from)) 
ackCallback.invoke(ack)
+          case nack: Nack => if (isFromCurrentRemote(nack.from)) 
nackCallback.invoke(nack)
           case _          => // not interested
         }
       }
diff --git 
a/remote/src/test/scala/org/apache/pekko/remote/artery/SystemMessageDeliverySpec.scala
 
b/remote/src/test/scala/org/apache/pekko/remote/artery/SystemMessageDeliverySpec.scala
index 73dff9928d..ba87667451 100644
--- 
a/remote/src/test/scala/org/apache/pekko/remote/artery/SystemMessageDeliverySpec.scala
+++ 
b/remote/src/test/scala/org/apache/pekko/remote/artery/SystemMessageDeliverySpec.scala
@@ -287,6 +287,50 @@ class SystemMessageDeliverySpec extends 
AbstractSystemMessageDeliverySpec(System
       sink.expectComplete()
     }
 
+    "be ignored when ACK is from stale remote incarnation" in {
+      val replyProbe = TestProbe()
+      val controlSubject = new TestControlMessageSubject
+      val inboundContextB = new ManualReplyInboundContext(replyProbe.ref, 
addressB, controlSubject)
+      val inboundContextA = new TestInboundContext(addressB, controlSubject)
+      val outboundContextA =
+        
inboundContextA.association(addressB.address).asInstanceOf[TestOutboundContext]
+
+      // Drop msg-3 BEFORE inbound() so SystemMessageAcker never generates a 
real ACK for it.
+      // This prevents the real ACK(3) from overwriting ACK(2) in 
ManualReplyInboundContext.
+      val sink = send(sendCount = 3, resendInterval = 1.second, 
outboundContextA)
+        .via(drop(dropSeqNumbers = Vector(3L)))
+        .via(inbound(inboundContextB))
+        .map(_.message.asInstanceOf[TestSysMsg])
+        .runWith(TestSink[TestSysMsg]())
+
+      sink.request(100)
+      sink.expectNext(TestSysMsg("msg-1"))
+      sink.expectNext(TestSysMsg("msg-2"))
+
+      // Complete handshake so uniqueRemoteAddress is set to the current 
addressB
+      Await.result(outboundContextA.completeHandshake(addressB), 3.seconds)
+
+      // Deliver valid ACKs for msg-1 and msg-2 (msg-3 was dropped, no ACK 
generated for it)
+      replyProbe.expectMsg(Ack(1L, addressB))
+      inboundContextB.deliverLastReply()
+      replyProbe.expectMsg(Ack(2L, addressB))
+      inboundContextB.deliverLastReply()
+
+      // Inject a stale ACK from a previous incarnation (same address, 
different UID)
+      val staleAddress = UniqueAddress(addressB.address, addressB.uid + 1000)
+      controlSubject.sendControl(
+        InboundEnvelope(OptionVal.None, Ack(3L, staleAddress), OptionVal.None, 
staleAddress.uid, OptionVal.None))
+
+      // msg-3 should NOT have been cleared by the stale ACK,
+      // so it must be resent after the resend interval
+      sink.expectNext(3.seconds, TestSysMsg("msg-3"))
+
+      // Deliver the real ACK from the resend to complete the stream
+      replyProbe.expectMsg(3.seconds, Ack(3L, addressB))
+      inboundContextB.deliverLastReply()
+      sink.expectComplete()
+    }
+
     "deliver all during stress and random dropping" in {
       val N = 500
       val dropRate = 0.05


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

Reply via email to