All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eventstore.akka.cluster.ClusterDiscovererActor.scala Maven / Gradle / Ivy

package eventstore
package akka
package cluster

import java.net.InetSocketAddress
import scala.concurrent.{Await, Future}
import scala.util.Random.shuffle
import scala.util.control.NonFatal
import _root_.akka.actor._
import _root_.akka.actor.Status.Failure
import eventstore.core.syntax._
import eventstore.core.settings.ClusterSettings
import eventstore.core.cluster.{ResolveDns, MemberInfo, ClusterException, NodeState}

private[eventstore] class ClusterDiscovererActor(
    settings:    ClusterSettings,
    clusterInfo: ClusterInfoOf.FutureFunc
) extends Actor with ActorLogging {
  import ClusterDiscovererActor._
  import context.dispatcher
  import settings._

  var clients: Set[ActorRef] = Set()

  val rcvTerminated: Receive = {
    case Terminated(client) if clients contains client => clients = clients - client
  }

  override def preStart() = self ! Tick

  def receive = discovering(1, None)

  def discovering(attempt: Int, failed: Option[InetSocketAddress]): Receive = rcvTerminated or {
    case GetAddress(_) => addClient(sender())
    case Tick          => discover(attempt, failed, Nil)
  }

  def discovered(bestNode: MemberInfo, members: List[MemberInfo]): Receive = {
    context.system.scheduler.scheduleOnce(discoveryInterval, self, Tick)
    rcvTerminated or {
      case GetAddress(failed) =>
        val client = sender()
        addClient(client)
        failed match {
          case Some(addr) if bestNode.externalTcp == addr =>
            log.info("Cluster best node {} failed, reported by {}", bestNode, client)
            context become recovering(bestNode, members)

          case _ => client ! Address(bestNode)
        }

      case Tick =>
        try {
          val future = this.clusterInfo(bestNode.externalHttp)
          val clusterInfo = Await.result(future, gossipTimeout)
          clusterInfo.bestNode match {
            case Some(newBestNode) =>
              if (newBestNode like bestNode) {
                val state = bestNode.state
                val newState = newBestNode.state
                if (state != newState) log.info("Cluster best node state changed from {} to {}", state, newState)
              } else {
                log.info("Cluster best node changed from {} to {}", bestNode, newBestNode)
                broadcast(Address(newBestNode))
              }
              context become discovered(newBestNode, clusterInfo.members)

            case None =>
              log.info("Cluster best node {} failed", bestNode)
              bestNodeFailed(bestNode, members)
          }
        } catch {
          case NonFatal(e) =>
            log.info("Failed to reach cluster best node {} with error: {}", bestNode, e)
            bestNodeFailed(bestNode, members)
        }
    }
  }

  def recovering(failed: MemberInfo, members: List[MemberInfo]): Receive = rcvTerminated or {
    case GetAddress(_) => addClient(sender())
    case Tick          => bestNodeFailed(failed, members)
  }

  def discover(attempt: Int, failed: Option[InetSocketAddress], seeds: List[InetSocketAddress]) = {

    def attemptFailed(e: Option[Throwable]) = {
      if (attempt < maxDiscoverAttempts) {
        e match {
          case Some(th) => log.info("Discovering cluster: attempt {}/{} failed with error: {}", attempt, maxDiscoverAttempts, th)
          case None     => log.info("Discovering cluster: attempt {}/{} failed: no candidate found", attempt, maxDiscoverAttempts)
        }
        context.system.scheduler.scheduleOnce(discoverAttemptInterval, self, Tick)
        context become discovering(attempt + 1, failed)
      } else {
        val msg = e match {
          case Some(th) => s"Failed to discover candidate in $maxDiscoverAttempts attempts with error: $th"
          case None     => s"Failed to discover candidate in $maxDiscoverAttempts attempts"
        }
        log.error(msg)
        broadcast(Failure(new ClusterException(msg, e)))
        context stop self
      }
    }

    try {
      val gossipSeeds = {
        if (seeds.nonEmpty) seeds
        else {
          val candidates = GossipCandidates(settings)
          failed.fold(candidates)(failed => seeds.filterNot(_ == failed)) match {
            case Nil => candidates
            case ss  => ss
          }
        }
      }

      val futures = gossipSeeds.map { gossipSeed =>
        val future = clusterInfo(gossipSeed)
        future.failed foreach { x =>
          log.debug("Failed to get cluster info from {}: {}", gossipSeed, x)
        }
        future
      }

      val future = Future.find(futures)(_.bestNode.isDefined)
      Await.result(future, gossipTimeout) match {
        case None => attemptFailed(None)
        case Some(ci) =>
          val bestNode = ci.bestNode.get
          log.info("Discovering cluster: attempt {}/{} successful: best candidate is {}", attempt, maxDiscoverAttempts, bestNode)
          broadcast(Address(bestNode))
          context become discovered(bestNode, ci.members)
      }
    } catch { case NonFatal(e) => attemptFailed(Some(e)) }
  }

  def addClient(client: ActorRef) = clients = clients + context.watch(client)

  def broadcast(x: AnyRef) = clients.foreach { client => client ! x }

  def bestNodeFailed(bestNode: MemberInfo, members: List[MemberInfo]) = {
    val seeds = GossipCandidates(members.filterNot(_ like bestNode))
    discover(1, Some(bestNode.externalHttp), seeds)
  }

  case object Tick

  object GossipCandidates {
    def apply(settings: ClusterSettings): List[InetSocketAddress] = {
      import eventstore.core.cluster.GossipSeedsOrDns._
      val gossipSeeds = settings.gossipSeedsOrDns match {
        case GossipSeeds(x)        => x
        case ClusterDns(dns, port) => ResolveDns(dns, dnsLookupTimeout).map(x => x :: port)
      }
      shuffle(gossipSeeds)
    }

    def apply(members: List[MemberInfo]): List[InetSocketAddress] = {
      val (nodes, managers) = members.filter(_.isAlive).partition(_.state != NodeState.Manager)
      (shuffle(nodes) ::: shuffle(managers)).map(_.externalHttp)
    }
  }
}

private[eventstore] object ClusterDiscovererActor {

  def props(settings: ClusterSettings, clusterInfo: ClusterInfoOf.FutureFunc): Props = {
    Props(new ClusterDiscovererActor(settings, clusterInfo))
  }

  @SerialVersionUID(1L) final case class GetAddress(failed: Option[InetSocketAddress])
  object GetAddress {
    def apply(): GetAddress = GetAddress(None)
  }

  @SerialVersionUID(1L) final case class Address(value: InetSocketAddress)

  object Address {
    def apply(x: MemberInfo): Address = if (x.externalTcp.getPort == 0) Address(x.externalSecureTcp) else Address(x.externalTcp)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy