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

pl.immutables.akka.reasonable.downing.StaticQuorumDowning.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017 Mateusz Bilski
 *
 * Licensed 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 pl.immutables.akka.reasonable.downing

import akka.actor.{ Actor, ActorLogging, ActorSystem, Cancellable, Props }
import akka.cluster.ClusterEvent._
import akka.cluster.{ Cluster, DowningProvider, Member, MemberStatus }

import scala.concurrent.ExecutionContext
import scala.concurrent.duration.FiniteDuration
import java.util.concurrent.TimeUnit

import scala.collection.JavaConverters._
import com.typesafe.config.Config

class StaticQuorumDowningProvider(system: ActorSystem) extends DowningProvider {
  import scala.concurrent.ExecutionContext.Implicits.global

  val settings = StaticQuorumDowningSettings(system.settings.config)

  override def downRemovalMargin: FiniteDuration = settings.stableAfter

  override def downingActorProps: Option[Props] =
    Some(StaticQuorumDowning.props(Cluster(system), settings))
}

object StaticQuorumDowning {
  case object QuorumCheck
  case object MemberCheck

  def props(cluster: Cluster, settings: StaticQuorumDowningSettings)(
      implicit ex: ExecutionContext
  ) = Props(new StaticQuorumDowning(cluster, settings))
}

case class StaticQuorumDowningSettings(quorum: Int, stableAfter: FiniteDuration, roles: Seq[String])

object StaticQuorumDowningSettings {
  def apply(conf: Config): StaticQuorumDowningSettings = StaticQuorumDowningSettings(
    quorum = conf.getInt("akka.reasonable.downing.quorum-size"),
    stableAfter = FiniteDuration(conf.getDuration("akka.reasonable.downing.stable-after").toMillis,
                                 TimeUnit.MILLISECONDS),
    roles =
      if (conf.hasPath("akka.reasonable.downing.quorum-roles"))
        conf.getStringList("akka.reasonable.downing.quorum-roles").asScala
      else Nil
  )
}

class StaticQuorumDowning(cluster: Cluster, settings: StaticQuorumDowningSettings)(
    implicit ex: ExecutionContext
) extends Actor
    with ActorLogging {

  import StaticQuorumDowning._
  log.info("Starting StaticQuorumDowning [{}]", settings)

  var quorumCheck: Option[Cancellable] = None

  var memberCheck: Option[Cancellable] =
    Some(
      context.system.scheduler
        .schedule(settings.stableAfter, settings.stableAfter, self, MemberCheck)
    )

  val quorumRoles = settings.roles.toSet

  override def preStart(): Unit =
    cluster.subscribe(self,
                      initialStateMode = InitialStateAsEvents,
                      classOf[MemberEvent],
                      classOf[UnreachableMember])

  override def postStop(): Unit = cluster.unsubscribe(self)

  def scheduleQuorumCheck(): Unit = {
    quorumCheck.foreach(_.cancel())
    quorumCheck = Some(
      context.system.scheduler.scheduleOnce(settings.stableAfter, self, QuorumCheck)
    )
  }

  def suitable(m: Member) = quorumRoles.isEmpty || quorumRoles.intersect(m.roles).nonEmpty

  def checkIfClusterStarted(): Unit =
    if (nodesOf(MemberStatus.Up).count(suitable) >= settings.quorum) {
      log.debug("Cluster reached minimal number of members.")
      memberCheck.map(_.cancel())
      memberCheck = None
      context.become(startedCluster)
    } else {
      log.debug("Waiting for cluster to reach minimal number of members.")
    }

  def checkQuorum(): Unit =
    if (unreachable.count(suitable) >= settings.quorum) {
      log.warning(
        "Downing reachable nodes because of {} unreachable nodes with roles [{}] [state={}]",
        settings.quorum,
        quorumRoles.mkString(", "),
        cluster.state
      )
      reachable.map(_.address).foreach(cluster.down)
    } else if (reachable.count(suitable) < settings.quorum) {
      log.warning("Downing reachable nodes because of too small cluster [state={}]", cluster.state)
      reachable.map(_.address).foreach(cluster.down)
    } else if (cluster.state.unreachable.nonEmpty) {
      if (isLeader) {
        log.warning("Downing unreachable nodes [state={}]", cluster.state)
        unreachable.map(_.address).foreach(cluster.down)
      } else if (isLeaderUp) {
        log.debug(
          "There are unreachable nodes but this node is not a leader. Doing nothing. [state={}]",
          cluster.state
        )
      } else {
        log.warning(
          "There are unreachable nodes and there is no leader. Downing unreachable nodes. [state={}]",
          cluster.state
        )
        unreachable.map(_.address).foreach(cluster.down)
      }
    } else {
      log.debug("Cluster is in a valid state [state={}]", cluster.state)
    }

  def startingCluster: Receive = {
    case MemberUp(_) =>
      checkIfClusterStarted()

    case MemberCheck =>
      checkIfClusterStarted()

    case _ => // ignore
  }

  def startedCluster: Receive = {
    case MemberUp(_) =>
      scheduleQuorumCheck()

    case MemberRemoved(_, _) =>
      scheduleQuorumCheck()

    case UnreachableMember(_) =>
      scheduleQuorumCheck()

    case QuorumCheck =>
      checkQuorum()

    case _ => // ignore
  }

  def weaklyUp    = nodesOf(MemberStatus.weaklyUp)
  def unreachable = cluster.state.unreachable
  def reachable   = cluster.state.members.diff(unreachable).diff(weaklyUp)
  def isLeader    = cluster.state.leader.contains(cluster.selfUniqueAddress.address)
  def isLeaderUp  = reachable.map(_.address).intersect(cluster.state.leader.toSet).nonEmpty

  def nodesOf(status: MemberStatus) = cluster.state.members.filter(_.status == status)

  override def receive: Receive = startingCluster
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy