org.apache.pekko.remote.artery.Handshake.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pekko-remote_2.12 Show documentation
Show all versions of pekko-remote_2.12 Show documentation
Apache Pekko is a toolkit for building highly concurrent, distributed, and resilient message-driven applications for Java and Scala.
The newest version!
/*
* 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) 2016-2022 Lightbend Inc.
*/
package org.apache.pekko.remote.artery
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.control.NoStackTrace
import org.apache.pekko
import pekko.Done
import pekko.actor.ActorSystem
import pekko.actor.Address
import pekko.dispatch.ExecutionContexts
import pekko.remote.UniqueAddress
import pekko.stream.Attributes
import pekko.stream.FlowShape
import pekko.stream.Inlet
import pekko.stream.Outlet
import pekko.stream.stage._
import pekko.util.OptionVal
import pekko.util.unused
/**
* INTERNAL API
*/
private[remote] object OutboundHandshake {
/**
* Stream is failed with this exception if the handshake is not completed
* within the handshake timeout.
*/
class HandshakeTimeoutException(msg: String) extends RuntimeException(msg) with NoStackTrace
final case class HandshakeReq(from: UniqueAddress, to: Address) extends ControlMessage
final case class HandshakeRsp(from: UniqueAddress) extends Reply
private sealed trait HandshakeState
private case object Start extends HandshakeState
private case object ReqInProgress extends HandshakeState
private case object Completed extends HandshakeState
private case object HandshakeTimeout
private case object HandshakeRetryTick
private case object InjectHandshakeTick
private case object LivenessProbeTick
}
/**
* INTERNAL API
*/
private[remote] class OutboundHandshake(
@unused system: ActorSystem,
outboundContext: OutboundContext,
outboundEnvelopePool: ObjectPool[ReusableOutboundEnvelope],
timeout: FiniteDuration,
retryInterval: FiniteDuration,
injectHandshakeInterval: FiniteDuration,
livenessProbeInterval: Duration)
extends GraphStage[FlowShape[OutboundEnvelope, OutboundEnvelope]] {
val in: Inlet[OutboundEnvelope] = Inlet("OutboundHandshake.in")
val out: Outlet[OutboundEnvelope] = Outlet("OutboundHandshake.out")
override val shape: FlowShape[OutboundEnvelope, OutboundEnvelope] = FlowShape(in, out)
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
new TimerGraphStageLogic(shape) with InHandler with OutHandler with StageLogging {
import OutboundHandshake._
private var handshakeState: HandshakeState = Start
private var pendingMessage: OptionVal[OutboundEnvelope] = OptionVal.None
private var injectHandshakeTickScheduled = false
private val uniqueRemoteAddressAsyncCallback = getAsyncCallback[UniqueAddress] { _ =>
if (handshakeState != Completed) {
handshakeCompleted()
if (isAvailable(out))
pull(in)
}
}
// this must be a `val` because function equality is used when removing in postStop
private val uniqueRemoteAddressListener: UniqueAddress => Unit =
peer => uniqueRemoteAddressAsyncCallback.invoke(peer)
override protected def logSource: Class[_] = classOf[OutboundHandshake]
override def preStart(): Unit = {
scheduleOnce(HandshakeTimeout, timeout)
livenessProbeInterval match {
case d: FiniteDuration => scheduleWithFixedDelay(LivenessProbeTick, d, d)
case _ => // only used in control stream
}
}
override def postStop(): Unit = {
outboundContext.associationState.removeUniqueRemoteAddressListener(uniqueRemoteAddressListener)
super.postStop()
}
// InHandler
override def onPush(): Unit = {
if (handshakeState != Completed)
throw new IllegalStateException(s"onPush before handshake completed, was [$handshakeState].")
// inject a HandshakeReq once in a while to trigger a new handshake when destination
// system has been restarted
if (injectHandshakeTickScheduled) {
// out is always available here, except for if a liveness HandshakeReq was just pushed
if (isAvailable(out))
push(out, grab(in))
else {
if (pendingMessage.isDefined)
throw new IllegalStateException(s"pendingMessage expected to be empty")
pendingMessage = OptionVal.Some(grab(in))
}
} else {
pushHandshakeReq()
pendingMessage = OptionVal.Some(grab(in))
}
}
// OutHandler
override def onPull(): Unit = {
handshakeState match {
case Completed =>
pendingMessage match {
case OptionVal.Some(p) =>
push(out, p)
pendingMessage = OptionVal.None
case _ =>
if (!hasBeenPulled(in))
pull(in)
}
case Start =>
outboundContext.associationState.uniqueRemoteAddress() match {
case Some(_) =>
handshakeCompleted()
case None =>
// will pull when handshake reply is received (uniqueRemoteAddress populated)
handshakeState = ReqInProgress
scheduleWithFixedDelay(HandshakeRetryTick, retryInterval, retryInterval)
// The InboundHandshake stage will complete the AssociationState.uniqueRemoteAddress
// when it receives the HandshakeRsp reply
outboundContext.associationState.addUniqueRemoteAddressListener(uniqueRemoteAddressListener)
}
// always push a HandshakeReq as the first message
pushHandshakeReq()
case ReqInProgress => // will pull when handshake reply is received
}
}
private def pushHandshakeReq(): Unit = {
injectHandshakeTickScheduled = true
scheduleOnce(InjectHandshakeTick, injectHandshakeInterval)
outboundContext.associationState.lastUsedTimestamp.set(System.nanoTime())
if (isAvailable(out))
push(out, createHandshakeReqEnvelope())
}
private def pushLivenessProbeReq(): Unit = {
// The associationState.lastUsedTimestamp will be updated when the HandshakeRsp is received
// and that is the confirmation that the other system is alive, and will not be quarantined
// by the quarantine-idle-outbound-after even though no real messages have been sent.
if (handshakeState == Completed && isAvailable(out) && pendingMessage.isEmpty) {
val lastUsedDuration = (System.nanoTime() - outboundContext.associationState.lastUsedTimestamp.get()).nanos
if (lastUsedDuration >= livenessProbeInterval) {
log.info(
"Association to [{}] has been idle for [{}] seconds, sending HandshakeReq to validate liveness",
outboundContext.remoteAddress,
lastUsedDuration.toSeconds)
push(out, createHandshakeReqEnvelope())
}
}
}
private def createHandshakeReqEnvelope(): OutboundEnvelope = {
outboundEnvelopePool
.acquire()
.init(
recipient = OptionVal.None,
message = HandshakeReq(outboundContext.localAddress, outboundContext.remoteAddress),
sender = OptionVal.None)
}
private def handshakeCompleted(): Unit = {
handshakeState = Completed
cancelTimer(HandshakeRetryTick)
cancelTimer(HandshakeTimeout)
}
override protected def onTimer(timerKey: Any): Unit =
timerKey match {
case InjectHandshakeTick =>
// next onPush message will trigger sending of HandshakeReq
injectHandshakeTickScheduled = false
case LivenessProbeTick =>
pushLivenessProbeReq()
case HandshakeRetryTick =>
if (isAvailable(out))
pushHandshakeReq()
case HandshakeTimeout =>
failStage(
new HandshakeTimeoutException(
s"Handshake with [${outboundContext.remoteAddress}] did not complete within ${timeout.toMillis} ms"))
case unknown =>
throw new IllegalArgumentException(s"Unknown timer key: $unknown")
}
setHandlers(in, out, this)
}
}
/**
* INTERNAL API
*/
private[remote] class InboundHandshake(inboundContext: InboundContext, inControlStream: Boolean)
extends GraphStage[FlowShape[InboundEnvelope, InboundEnvelope]] {
val in: Inlet[InboundEnvelope] = Inlet("InboundHandshake.in")
val out: Outlet[InboundEnvelope] = Outlet("InboundHandshake.out")
override val shape: FlowShape[InboundEnvelope, InboundEnvelope] = FlowShape(in, out)
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
new TimerGraphStageLogic(shape) with OutHandler with StageLogging {
import OutboundHandshake._
private val runInStage = getAsyncCallback[() => Unit] { thunk =>
thunk()
}
// InHandler
if (inControlStream)
setHandler(
in,
new InHandler {
override def onPush(): Unit = {
val env = grab(in)
env.message match {
case HandshakeReq(from, to) => onHandshakeReq(from, to)
case HandshakeRsp(from) =>
// Touch the lastUsedTimestamp here also because when sending the extra low frequency HandshakeRsp
// the timestamp is not supposed to be updated when sending but when receiving reply, which confirms
// that the other system is alive.
inboundContext.association(from.address).associationState.lastUsedTimestamp.set(System.nanoTime())
after(inboundContext.completeHandshake(from)) { () =>
pull(in)
}
case _ =>
onMessage(env)
}
}
})
else
setHandler(in,
new InHandler {
override def onPush(): Unit = {
val env = grab(in)
env.message match {
case HandshakeReq(from, to) => onHandshakeReq(from, to)
case _ =>
onMessage(env)
}
}
})
private def onHandshakeReq(from: UniqueAddress, to: Address): Unit = {
if (to == inboundContext.localAddress.address) {
after(inboundContext.completeHandshake(from)) { () =>
inboundContext.sendControl(from.address, HandshakeRsp(inboundContext.localAddress))
pull(in)
}
} else {
log.warning(
"Dropping Handshake Request from [{}] addressed to unknown local address [{}]. " +
"Local address is [{}]. Check that the sending system uses the same " +
"address to contact recipient system as defined in the " +
"'pekko.remote.artery.canonical.hostname' of the recipient system. " +
"The name of the ActorSystem must also match.",
from,
to,
inboundContext.localAddress.address)
pull(in)
}
}
private def after(first: Future[Done])(thenInside: () => Unit): Unit = {
first.value match {
case Some(_) =>
// This in the normal case (all but the first). The future will be completed
// because handshake was already completed. Note that we send those HandshakeReq
// periodically.
thenInside()
case None =>
first.onComplete(_ => runInStage.invoke(thenInside))(ExecutionContexts.parasitic)
}
}
private def onMessage(env: InboundEnvelope): Unit = {
if (isKnownOrigin(env))
push(out, env)
else {
val dropReason = s"Unknown system with UID [${env.originUid}]. " +
s"This system with UID [${inboundContext.localAddress.uid}] was probably restarted. " +
"Messages will be accepted when new handshake has been completed."
inboundContext.publishDropped(env, dropReason)
pull(in)
}
}
private def isKnownOrigin(env: InboundEnvelope): Boolean = {
// the association is passed in the envelope from the Decoder stage to avoid
// additional lookup. The second OR case is because if we didn't use fusing it
// would be possible that it was not found by Decoder (handshake not completed yet)
env.association.isDefined || inboundContext.association(env.originUid).isDefined
}
// OutHandler
override def onPull(): Unit = pull(in)
setHandler(out, this)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy