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 3b21abca49 fix(remote): resume classic remoting reader after failed 
handoff (#3205)
3b21abca49 is described below

commit 3b21abca495d00d53229f182ba510b28ca4101f3
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sun Jul 5 21:17:27 2026 +0800

    fix(remote): resume classic remoting reader after failed handoff (#3205)
    
    Motivation:
    Classic remoting passive read handoff can leave the original outbound 
reader stuck in notReading if the read-only handoff endpoint fails.
    
    Modification:
    Track passive read handoff fallback endpoints and resume the original 
writable reader when the read-only endpoint terminates while the original 
endpoint and UID still match. Carry fallback state across replacement read-only 
endpoints, clear stale fallback state on UID/address changes, handle 
ResumeReading in supervisor states, add logging, and add regression coverage. 
Also correct the stale classic remoting passive-connections config text.
    
    Result:
    Classic remoting can recover future inbound messages on the original 
association after a failed passive read handoff.
    
    Tests:
    - rtk sbt "remote / Test / testOnly 
org.apache.pekko.remote.ReliableDeliverySupervisorSpec" / passed (4 tests)
    - rtk sbt "remote / Test / testOnly 
org.apache.pekko.remote.classic.RemotingSpec -- -z \"resume the outbound 
reader\"" / passed
    - rtk sbt "remote / Test / testOnly 
org.apache.pekko.remote.classic.RemotingSpec" / passed (22 tests)
    - rtk scalafmt --list --mode diff-ref=origin/main / passed (JVM Unsafe 
warnings only)
    - rtk bash -lc 'git diff --check' / passed
    - rtk sbt sortImports / failed: Scalafix/scala-meta NoSuchMethodError in 
local tooling; unrelated generated change was reverted
    - qodercli stdout review / No must-fix findings
    - independent subAgent review / No must-fix findings
    
    References:
    Fixes #3206
---
 .../scala/org/apache/pekko/remote/Endpoint.scala   |  46 +++++++--
 .../scala/org/apache/pekko/remote/Remoting.scala   |  85 ++++++++++++++--
 .../remote/ReliableDeliverySupervisorSpec.scala    | 101 ++++++++++++++++---
 .../apache/pekko/remote/classic/RemotingSpec.scala | 111 +++++++++++++++++++++
 4 files changed, 313 insertions(+), 30 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 0c39a1e4de..8e0208bade 100644
--- a/remote/src/main/scala/org/apache/pekko/remote/Endpoint.scala
+++ b/remote/src/main/scala/org/apache/pekko/remote/Endpoint.scala
@@ -387,6 +387,9 @@ private[remote] class ReliableDeliverySupervisor(
     case s: EndpointWriter.StopReading =>
       writer.forward(s)
 
+    case EndpointWriter.ResumeReading =>
+      writer ! EndpointWriter.ResumeReading
+
     case Ungate => // ok, not gated
   }
 
@@ -418,10 +421,18 @@ private[remote] class ReliableDeliverySupervisor(
         // Resending will be triggered by the incoming GotUid message after 
the connection finished
         goToActive()
       } else goToIdle()
-    case AttemptSysMsgRedelivery                => // Ignore
-    case s @ Send(_: SystemMessage, _, _, _)    => tryBuffer(s.copy(seqOpt = 
Some(nextSeq())))
-    case s: Send                                => context.system.deadLetters 
! s
-    case EndpointWriter.FlushAndStop            => context.stop(self)
+    case AttemptSysMsgRedelivery             => // Ignore
+    case s @ Send(_: SystemMessage, _, _, _) => tryBuffer(s.copy(seqOpt = 
Some(nextSeq())))
+    case s: Send                             => context.system.deadLetters ! s
+    case EndpointWriter.FlushAndStop         => context.stop(self)
+    case EndpointWriter.ResumeReading        =>
+      if (!writerTerminated) {
+        writer ! EndpointWriter.ResumeReading
+        context.become(gated(writerTerminated = false, earlyUngateRequested = 
true))
+      } else {
+        writer = createWriter()
+        goToActive()
+      }
     case EndpointWriter.StopReading(w, replyTo) =>
       replyTo ! EndpointWriter.StoppedReading(w)
       sender() ! EndpointWriter.StoppedReading(w)
@@ -448,7 +459,10 @@ private[remote] class ReliableDeliverySupervisor(
         new TimeoutException(
           "Remote system has been silent for too long. " +
           s"(more than ${settings.QuarantineSilentSystemTimeout.toCoarsest})"))
-    case EndpointWriter.FlushAndStop            => context.stop(self)
+    case EndpointWriter.FlushAndStop  => context.stop(self)
+    case EndpointWriter.ResumeReading =>
+      writer = createWriter()
+      goToActive()
     case EndpointWriter.StopReading(w, replyTo) =>
       replyTo ! EndpointWriter.StoppedReading(w)
     case Ungate => // ok, not gated
@@ -482,7 +496,8 @@ private[remote] class ReliableDeliverySupervisor(
       // and don't really know if they were properly delivered or not.
       resendBuffer = new AckedSendBuffer[Send](0)
       context.stop(self)
-    case _ => // Ignore
+    case EndpointWriter.ResumeReading => // Ignore while shutting down
+    case _                            => // Ignore
   }
 
   private def handleSend(send: Send): Unit =
@@ -625,6 +640,7 @@ private[remote] object EndpointWriter {
   private case object FlushAndStopTimeout
   case object AckIdleCheckTimer
   final case class StopReading(writer: ActorRef, replyTo: ActorRef)
+  case object ResumeReading
   final case class StoppedReading(writer: ActorRef)
 
   final case class Handle(handle: PekkoProtocolHandle) extends 
NoSerializationVerificationNeeded
@@ -751,8 +767,10 @@ private[remote] class EndpointWriter(
   }
 
   val buffering: Receive = {
-    case s: Send      => enqueueInBuffer(s)
-    case BackoffTimer => sendBufferedMessages()
+    case s: Send       => enqueueInBuffer(s)
+    case BackoffTimer  => sendBufferedMessages()
+    case ResumeReading =>
+      reader.foreach(_ ! ResumeReading)
     case FlushAndStop =>
       // Flushing is postponed after the pending writes
       buffer.offer(FlushAndStop)
@@ -804,6 +822,9 @@ private[remote] class EndpointWriter(
       case s @ StopReading(_, replyTo) =>
         reader.foreach(_.tell(s, replyTo))
         true
+      case ResumeReading =>
+        reader.foreach(_ ! ResumeReading)
+        true
       case unexpected =>
         throw new IllegalArgumentException(s"Unexpected message type: 
${unexpected.getClass}")
     }
@@ -996,6 +1017,8 @@ private[remote] class EndpointWriter(
           // initializing, buffer and take care of it later when buffer is sent
           enqueueInBuffer(s)
       }
+    case ResumeReading =>
+      reader.foreach(_ ! ResumeReading)
     case TakeOver(newHandle, replyTo) =>
       // Shutdown old reader
       handle.foreach { _.disassociate("the association was replaced by a new 
one", log) }
@@ -1113,7 +1136,7 @@ private[remote] class EndpointReader(
     val receiveBuffers: ConcurrentHashMap[Link, ResendState])
     extends EndpointActor(localAddress, remoteAddress, transport, settings, 
codec) {
 
-  import EndpointWriter.{ OutboundAck, StopReading, StoppedReading }
+  import EndpointWriter.{ OutboundAck, ResumeReading, StopReading, 
StoppedReading }
 
   val provider = RARP(context.system).provider
   var ackedReceiveBuffer = new AckedReceiveBuffer[Message]
@@ -1186,6 +1209,8 @@ private[remote] class EndpointReader(
       context.become(notReading)
       replyTo ! StoppedReading(writer)
 
+    case ResumeReading => // already reading
+
   }
 
   private def logTransientSerializationError(msg: PekkoPduCodec.Message, 
error: Throwable): Unit = {
@@ -1204,6 +1229,9 @@ private[remote] class EndpointReader(
     case StopReading(writer, replyTo) =>
       replyTo ! StoppedReading(writer)
 
+    case ResumeReading =>
+      context.become(receive)
+
     case InboundPayload(p) if p.length <= transport.maximumPayloadBytes =>
       val (ackOption, msgOption) = tryDecodeMessageAndAck(p)
       for (ack <- ackOption; reliableDelivery <- reliableDeliverySupervisor)
diff --git a/remote/src/main/scala/org/apache/pekko/remote/Remoting.scala 
b/remote/src/main/scala/org/apache/pekko/remote/Remoting.scala
index 2aca007491..3aed2fcc75 100644
--- a/remote/src/main/scala/org/apache/pekko/remote/Remoting.scala
+++ b/remote/src/main/scala/org/apache/pekko/remote/Remoting.scala
@@ -373,6 +373,7 @@ private[remote] object EndpointManager {
   final case class Quarantined(uid: Int, timeOfRelease: Deadline) extends 
EndpointPolicy {
     override def isTombstone: Boolean = true
   }
+  private final case class ResumableReader(writer: ActorRef, remoteAddress: 
Address, uid: Int)
 
   // Not threadsafe -- only to be used in HeadActor
   class EndpointRegistry {
@@ -536,6 +537,7 @@ private[remote] class EndpointManager(conf: Config, log: 
LoggingAdapter)
     context.system.scheduler.scheduleWithFixedDelay(pruneInterval, 
pruneInterval, self, Prune)
 
   var pendingReadHandoffs = Map[ActorRef, PekkoProtocolHandle]()
+  private var readOnlyReaderResumptions = Map[ActorRef, ResumableReader]()
   var stashedInbound = Map[ActorRef, Vector[InboundAssociation]]()
 
   def handleStashedInbound(endpoint: ActorRef, writerIsIdle: Boolean): Unit = {
@@ -783,13 +785,17 @@ private[remote] class EndpointManager(conf: Config, log: 
LoggingAdapter)
     case ia @ InboundAssociation(_: PekkoProtocolHandle) =>
       handleInboundAssociation(ia, writerIsIdle = false)
     case EndpointWriter.StoppedReading(endpoint) =>
-      acceptPendingReader(takingOverFrom = endpoint)
+      acceptPendingReader(takingOverFrom = endpoint, resumeReadingFrom = 
Some(endpoint))
     case Terminated(endpoint) =>
-      acceptPendingReader(takingOverFrom = endpoint)
-      endpoints.unregisterEndpoint(endpoint)
+      if (pendingReadHandoffs.contains(endpoint))
+        endpoints.unregisterEndpoint(endpoint)
+      if (!acceptPendingReader(takingOverFrom = endpoint, resumeReadingFrom = 
None)) {
+        resumeReadingIfNeeded(readOnlyEndpoint = endpoint)
+        endpoints.unregisterEndpoint(endpoint)
+      }
       handleStashedInbound(endpoint, writerIsIdle = false)
     case EndpointWriter.TookOver(endpoint, handle) =>
-      removePendingReader(takingOverFrom = endpoint, withHandle = handle)
+      completePendingReaderTakeOver(takingOverFrom = endpoint, withHandle = 
handle)
     case ReliableDeliverySupervisor.GotUid(uid, remoteAddress) =>
       val refuseUidOption = endpoints.refuseUid(remoteAddress)
       endpoints.writableEndpointWithPolicyFor(remoteAddress) match {
@@ -959,7 +965,7 @@ private[remote] class EndpointManager(conf: Config, log: 
LoggingAdapter)
     })
   }
 
-  private def acceptPendingReader(takingOverFrom: ActorRef): Unit = {
+  private def acceptPendingReader(takingOverFrom: ActorRef, resumeReadingFrom: 
Option[ActorRef]): Boolean = {
     if (pendingReadHandoffs.contains(takingOverFrom)) {
       val handle = pendingReadHandoffs(takingOverFrom)
       pendingReadHandoffs -= takingOverFrom
@@ -973,12 +979,77 @@ private[remote] class EndpointManager(conf: Config, log: 
LoggingAdapter)
         Some(handle),
         writing = false)
       endpoints.registerReadOnlyEndpoint(handle.remoteAddress, endpoint, 
handle.handshakeInfo.uid)
+      val inheritedResumableReader = 
readOnlyReaderResumptions.get(takingOverFrom).filter(
+        resumableReaderMatchesHandle(_, readOnlyEndpoint = takingOverFrom, 
handle = handle))
+      val resumableReader = resumeReadingFrom
+        .map(writer => ResumableReader(writer, handle.remoteAddress, 
handle.handshakeInfo.uid))
+        .orElse(inheritedResumableReader)
+      readOnlyReaderResumptions -= takingOverFrom
+      resumableReader.foreach {
+        case ResumableReader(writer, remoteAddress, uid) =>
+          readOnlyReaderResumptions += endpoint -> ResumableReader(writer, 
remoteAddress, uid)
+          log.debug(
+            "Registered passive read handoff fallback for [{}] with UID [{}] 
from writable endpoint [{}] to read-only endpoint [{}]",
+            remoteAddress,
+            uid,
+            writer,
+            endpoint)
+      }
+      true
+    } else false
+  }
+
+  private def resumeReadingIfNeeded(readOnlyEndpoint: ActorRef): Unit = {
+    readOnlyReaderResumptions.get(readOnlyEndpoint).foreach {
+      case ResumableReader(writer, remoteAddress, uid) =>
+        endpoints.writableEndpointWithPolicyFor(remoteAddress) match {
+          case Some(Pass(`writer`, Some(`uid`))) =>
+            log.info(
+              "Resuming outbound reader [{}] for [{}] with UID [{}] after 
passive read-only endpoint [{}] stopped",
+              writer,
+              remoteAddress,
+              uid,
+              readOnlyEndpoint)
+            writer ! EndpointWriter.ResumeReading
+          case currentPolicy =>
+            log.debug(
+              "Not resuming outbound reader [{}] for [{}] with UID [{}] after 
passive read-only endpoint [{}] stopped",
+              writer,
+              remoteAddress,
+              uid,
+              readOnlyEndpoint)
+            log.debug(
+              "Current endpoint policy when skipping passive read handoff 
fallback for [{}] is [{}]",
+              remoteAddress,
+              currentPolicy)
+        }
     }
+    readOnlyReaderResumptions -= readOnlyEndpoint
   }
 
-  private def removePendingReader(takingOverFrom: ActorRef, withHandle: 
PekkoProtocolHandle): Unit = {
-    if (pendingReadHandoffs.get(takingOverFrom).exists(handle => handle == 
withHandle))
+  private def resumableReaderMatchesHandle(
+      resumableReader: ResumableReader,
+      readOnlyEndpoint: ActorRef,
+      handle: PekkoProtocolHandle): Boolean = {
+    val ResumableReader(writer, remoteAddress, uid) = resumableReader
+    val matchesCurrentHandle = remoteAddress == handle.remoteAddress && uid == 
handle.handshakeInfo.uid
+    if (!matchesCurrentHandle && log.isDebugEnabled)
+      log.debug(
+        s"""Dropping passive read handoff fallback for [$remoteAddress] with 
UID [$uid] from writable endpoint
+           |[$writer] because read-only endpoint [$readOnlyEndpoint] now 
handles [${handle.remoteAddress}]
+           |with UID [${handle.handshakeInfo.uid}]""".stripMargin)
+    matchesCurrentHandle
+  }
+
+  private def completePendingReaderTakeOver(takingOverFrom: ActorRef, 
withHandle: PekkoProtocolHandle): Unit = {
+    if (pendingReadHandoffs.get(takingOverFrom).exists(handle => handle == 
withHandle)) {
       pendingReadHandoffs -= takingOverFrom
+      endpoints.registerReadOnlyEndpoint(withHandle.remoteAddress, 
takingOverFrom, withHandle.handshakeInfo.uid)
+      if (!readOnlyReaderResumptions
+          .get(takingOverFrom)
+          .forall(resumableReaderMatchesHandle(_, readOnlyEndpoint = 
takingOverFrom, handle = withHandle)))
+        readOnlyReaderResumptions -= takingOverFrom
+    }
   }
 
   private def createEndpoint(
diff --git 
a/remote/src/test/scala/org/apache/pekko/remote/ReliableDeliverySupervisorSpec.scala
 
b/remote/src/test/scala/org/apache/pekko/remote/ReliableDeliverySupervisorSpec.scala
index d433f03bc0..4529eb21fe 100644
--- 
a/remote/src/test/scala/org/apache/pekko/remote/ReliableDeliverySupervisorSpec.scala
+++ 
b/remote/src/test/scala/org/apache/pekko/remote/ReliableDeliverySupervisorSpec.scala
@@ -21,9 +21,10 @@ import java.util.concurrent.ConcurrentHashMap
 
 import scala.annotation.nowarn
 import scala.concurrent.Promise
+import scala.concurrent.duration._
 
 import org.apache.pekko
-import pekko.actor.{ ActorRef, Address, Nobody, RootActorPath }
+import pekko.actor.{ ActorRef, Address, Nobody, RootActorPath, Terminated }
 import pekko.remote.EndpointManager.{ Link, ResendState, Send }
 import pekko.remote.ReliableDeliverySupervisor.AckFromReader
 import pekko.remote.transport._
@@ -90,6 +91,61 @@ class ReliableDeliverySupervisorSpec extends 
PekkoSpec(ReliableDeliverySuperviso
       underlying.resendBuffer.nonAcked.map(_.seq) should ===(Vector(SeqNo(1)))
       underlying.resendBuffer = new AckedSendBuffer[Send](0)
     }
+
+    "recreate the writer when resuming reading while idle" in {
+      val parentProbe = TestProbe()
+      val supervisor = newSupervisor(initialUid = oldUid, parentProbe.ref)
+      val oldWriter = supervisor.underlyingActor.writer
+      val writerProbe = TestProbe()
+
+      writerProbe.watch(oldWriter)
+      supervisor.unwatch(oldWriter)
+      system.stop(oldWriter)
+      writerProbe.expectTerminated(oldWriter)
+
+      supervisor.receive(Terminated(oldWriter)(existenceConfirmed = true, 
addressTerminated = false))
+      parentProbe.expectMsg(EndpointWriter.StoppedReading(supervisor))
+      supervisor.underlyingActor.currentHandle = 
Some(newProtocolHandle(oldUid))
+
+      supervisor.receive(EndpointWriter.ResumeReading)
+
+      supervisor.underlyingActor.writer should not be oldWriter
+      expectActive(supervisor)
+    }
+
+    "recreate the writer when resuming reading while gated" in {
+      val parentProbe = TestProbe()
+      val supervisor = newSupervisor(initialUid = oldUid, parentProbe.ref)
+      val underlying = supervisor.underlyingActor
+      val oldWriter = supervisor.underlyingActor.writer
+      val writerProbe = TestProbe()
+
+      writerProbe.watch(oldWriter)
+      supervisor.unwatch(oldWriter)
+      system.stop(oldWriter)
+      writerProbe.expectTerminated(oldWriter)
+      underlying.currentHandle = Some(newProtocolHandle(oldUid))
+      underlying.context.become(underlying.gated(writerTerminated = true, 
earlyUngateRequested = false))
+
+      supervisor.receive(EndpointWriter.ResumeReading)
+
+      supervisor.underlyingActor.writer should not be oldWriter
+      expectActive(supervisor)
+    }
+
+    "forward resume reading while gated before the writer terminates" in {
+      val parentProbe = TestProbe()
+      val supervisor = newSupervisor(initialUid = oldUid, parentProbe.ref)
+      val underlying = supervisor.underlyingActor
+      val writerProbe = TestProbe()
+
+      underlying.writer = writerProbe.ref
+      underlying.context.become(underlying.gated(writerTerminated = false, 
earlyUngateRequested = false))
+
+      supervisor.receive(EndpointWriter.ResumeReading)
+
+      writerProbe.expectMsg(EndpointWriter.ResumeReading)
+    }
   }
 
   private def newSupervisor(initialUid: Int, parent: ActorRef): 
TestActorRef[ReliableDeliverySupervisor] = {
@@ -102,22 +158,10 @@ class ReliableDeliverySupervisorSpec extends 
PekkoSpec(ReliableDeliverySuperviso
       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),
+        handleOrActive = Some(newProtocolHandle(initialUid, 
underlyingTransport)),
         localAddress,
         remoteAddress,
         refuseUid = None,
@@ -128,6 +172,35 @@ class ReliableDeliverySupervisorSpec extends 
PekkoSpec(ReliableDeliverySuperviso
       parent)
   }
 
+  private def expectActive(supervisor: ActorRef): Unit = {
+    val idleProbe = TestProbe()
+    supervisor.tell(ReliableDeliverySupervisor.IsIdle, idleProbe.ref)
+    idleProbe.expectNoMessage(100.millis)
+  }
+
+  private def newProtocolHandle(uid: Int): PekkoProtocolHandle =
+    newProtocolHandle(
+      uid,
+      new TestTransport(
+        localAddress.copy(protocol = "test"),
+        new TestTransport.AssociationRegistry,
+        schemeIdentifier = "test"))
+
+  private def newProtocolHandle(uid: Int, underlyingTransport: TestTransport): 
PekkoProtocolHandle = {
+    val underlyingHandle =
+      TestAssociationHandle(localAddress.copy(protocol = "test"), 
remoteAddress.copy(protocol = "test"),
+        underlyingTransport, inbound = true)
+    new PekkoProtocolHandle(
+      localAddress,
+      remoteAddress,
+      Promise[AssociationHandle.HandleEventListener](),
+      underlyingHandle,
+      HandshakeInfo(remoteAddress, uid),
+      TestProbe().ref,
+      PekkoPduProtobufCodec,
+      RARP(system).provider.remoteSettings.ProtocolName)
+  }
+
   private def bufferWith(seqNumbers: Long*): AckedSendBuffer[Send] =
     seqNumbers.foldLeft(new AckedSendBuffer[Send](10)) { (buffer, seq) =>
       buffer.buffer(send(seq))
diff --git 
a/remote/src/test/scala/org/apache/pekko/remote/classic/RemotingSpec.scala 
b/remote/src/test/scala/org/apache/pekko/remote/classic/RemotingSpec.scala
index 0fb42195e8..cbed60cd92 100644
--- a/remote/src/test/scala/org/apache/pekko/remote/classic/RemotingSpec.scala
+++ b/remote/src/test/scala/org/apache/pekko/remote/classic/RemotingSpec.scala
@@ -688,6 +688,117 @@ class RemotingSpec extends PekkoSpec(RemotingSpec.cfg) 
with ImplicitSender with
 
     }
 
+    "should resume the outbound reader when passive read handoff fails" in {
+      val localAddress = Address("pekko.test", "resume-system1", "localhost", 
101)
+      val rawLocalAddress = localAddress.copy(protocol = "test")
+      val remoteAddress = Address("pekko.test", "resume-system2", "localhost", 
102)
+      val rawRemoteAddress = remoteAddress.copy(protocol = "test")
+      val remoteUID = 17
+      val receiverName = "handoff-receiver"
+
+      val config = ConfigFactory.parseString(s"""
+        pekko.remote.classic.enabled-transports = ["pekko.remote.classic.test"]
+        pekko.remote.classic.retry-gate-closed-for = 5s
+        pekko.remote.classic.log-remote-lifecycle-events = on
+
+        pekko.remote.classic.test {
+          registry-key = TwHFcESm
+          local-address = 
"test://${localAddress.system}@${localAddress.host.get}:${localAddress.port.get}"
+        }
+        """).withFallback(remoteSystem.settings.config)
+      val thisSystem = ActorSystem("this-system", config)
+      muteSystem(thisSystem)
+
+      try {
+        val receiverProbe = TestProbe()(thisSystem)
+        thisSystem.actorOf(Props(new Actor {
+            def receive = {
+              case message => receiverProbe.ref ! message
+            }
+          }).withDeploy(Deploy.local), receiverName)
+        val associationEventProbe = TestProbe()(thisSystem)
+        thisSystem.eventStream.subscribe(associationEventProbe.ref, 
classOf[AssociatedEvent])
+
+        val registry = AssociationRegistry.get("TwHFcESm")
+        val remoteTransport = new TestTransport(rawRemoteAddress, registry)
+        val remoteTransportProbe = TestProbe()
+
+        registry.registerTransport(
+          remoteTransport,
+          associationEventListenerFuture = Future.successful(new 
Transport.AssociationEventListener {
+            override def notify(ev: Transport.AssociationEvent): Unit =
+              remoteTransportProbe.ref ! ev
+          }))
+
+        awaitCond(registry.transportsReady(rawLocalAddress, rawRemoteAddress))
+
+        val dummySelection =
+          
thisSystem.actorSelection(ActorPath.fromString(remoteAddress.toString + 
"/user/noonethere"))
+        dummySelection.tell("ping", system.deadLetters)
+
+        val outboundRemoteHandle = 
remoteTransportProbe.expectMsgType[Transport.InboundAssociation]
+        val outboundHandleProbe = TestProbe()
+        outboundRemoteHandle.association.readHandlerPromise.success(new 
HandleEventListener {
+          override def notify(ev: HandleEvent): Unit = outboundHandleProbe.ref 
! ev
+        })
+
+        def handshakePacket(uid: Int): ByteString =
+          
PekkoPduProtobufCodec.constructAssociate(HandshakeInfo(rawRemoteAddress, uid))
+        val originalHandshakePacket = handshakePacket(remoteUID)
+        outboundHandleProbe.fishForMessage(3.seconds, "outbound handshake") {
+          case AssociationHandle.InboundPayload(_) => true
+          case _                                   => false
+        }
+        outboundRemoteHandle.association.write(originalHandshakePacket)
+
+        def payload(message: String): ByteString = {
+          val serializedMessage =
+            
MessageSerializer.serialize(thisSystem.asInstanceOf[ExtendedActorSystem], 
message)
+          val recipient = new EmptyLocalActorRef(
+            thisSystem.asInstanceOf[ExtendedActorSystem].provider,
+            RootActorPath(localAddress) / "user" / receiverName,
+            thisSystem.eventStream)
+
+          PekkoPduProtobufCodec.constructPayload(
+            PekkoPduProtobufCodec.constructMessage(
+              localAddress,
+              recipient,
+              serializedMessage,
+              pekko.util.OptionVal.None))
+        }
+
+        outboundRemoteHandle.association.write(payload("before-handoff"))
+        receiverProbe.expectMsg("before-handoff")
+
+        val inboundHandleProbe = TestProbe()
+        val inboundHandle = 
Await.result(remoteTransport.associate(rawLocalAddress), 3.seconds)
+        inboundHandle.readHandlerPromise.success(new 
AssociationHandle.HandleEventListener {
+          override def notify(ev: HandleEvent): Unit = inboundHandleProbe.ref 
! ev
+        })
+
+        awaitAssert {
+          
registry.getRemoteReadHandlerFor(inboundHandle.asInstanceOf[TestAssociationHandle]).get
+        }
+
+        inboundHandle.write(originalHandshakePacket)
+        associationEventProbe.fishForMessage(3.seconds, "passive association") 
{
+          case AssociatedEvent(`localAddress`, `remoteAddress`, true) => true
+          case _                                                      => false
+        }
+
+        val brokenPacket = 
PekkoPduProtobufCodec.constructPayload(ByteString(0, 1, 2, 3, 4, 5, 6))
+        inboundHandle.write(brokenPacket)
+        inboundHandleProbe.fishForMessage(3.seconds, "read-only endpoint 
disassociation") {
+          case _: AssociationHandle.Disassociated => true
+          case _                                  => false
+        }
+
+        
outboundRemoteHandle.association.write(payload("after-handoff-failure"))
+        receiverProbe.expectMsg("after-handoff-failure")
+      } finally shutdown(thisSystem)
+
+    }
+
     "should properly quarantine stashed inbound connections" in {
       val localAddress = Address("pekko.test", "system1", "localhost", 1)
       val rawLocalAddress = localAddress.copy(protocol = "test")


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

Reply via email to