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

com.teambytes.inflatable.raft.RaftActor.scala Maven / Gradle / Ivy

There is a newer version: 1.0.5
Show newest version
package com.teambytes.inflatable.raft

import akka.actor.{Actor, LoggingFSM}
import scala.concurrent.duration._

import model._
import protocol._
import scala.concurrent.forkjoin.ThreadLocalRandom
import com.teambytes.inflatable.raft.compaction.LogCompactionExtension
import com.teambytes.inflatable.raft.config.RaftConfiguration

private[inflatable] abstract class RaftActor extends Actor with LoggingFSM[RaftState, Metadata]
  with ReplicatedStateMachine with Follower with Candidate with Leader with SharedBehaviors {

  private[inflatable] type Command

  protected val raftConfig = RaftConfiguration(context.system)

  protected val logCompaction = LogCompactionExtension(context.system)

  private val ElectionTimeoutTimerName = "election-timer"

  /**
   * Used in order to avoid starting elections when we process an timeout event (from the election-timer),
   * which is immediatly followed by a message from the Leader.
   * This is because timeout handling is just a plain old message (no priority implemented there).
   *
   * This means our election reaction to timeouts is relaxed a bit - yes, but this should be a good thing.
   */
  var electionDeadline: Deadline = 0.seconds.fromNow


  // raft member state ---------------------

  var replicatedLog = ReplicatedLog.empty[Command](raftConfig.defaultAppendEntriesBatchSize)

  var nextIndex = LogIndexMap.initialize(Set.empty, replicatedLog.lastIndex)

  var matchIndex = LogIndexMap.initialize(Set.empty, -1)

  // end of raft member state --------------

  val heartbeatInterval: FiniteDuration = raftConfig.heartbeatInterval

  def nextElectionDeadline(): Deadline = randomElectionTimeout(
    from = raftConfig.electionTimeoutMin,
    to = raftConfig.electionTimeoutMax
  ).fromNow

  override def preStart() {
    log.info("Starting new Raft member, will wait for raft cluster configuration...")
  }

  startWith(Init, Meta.initial)

  when(Init)(initialConfigurationBehavior)

  when(Follower)(followerBehavior orElse snapshottingBehavior orElse clusterManagementBehavior)

  when(Candidate)(candidateBehavior orElse snapshottingBehavior orElse clusterManagementBehavior)

  when(Leader)(leaderBehavior orElse snapshottingBehavior orElse clusterManagementBehavior)

  onTransition {
    case Init -> Follower if stateData.clusterSelf != self =>
      log.info("Cluster self != self => Running clustered via a proxy.")
      resetElectionDeadline()

    case Follower -> Candidate =>
      self ! BeginElection
      resetElectionDeadline()

    case Candidate -> Leader =>
      self ! ElectedAsLeader
      cancelElectionDeadline()
      onIsLeader()

    case _ -> Follower =>
      resetElectionDeadline()
  }

  onTermination {
    case stop =>
      stopHeartbeat()
  }

  initialize() // akka internals; MUST be last call in constructor

  // helpers -----------------------------------------------------------------------------------------------------------

  def cancelElectionDeadline() {
    cancelTimer(ElectionTimeoutTimerName)
  }

  def resetElectionDeadline(): Deadline = {
    cancelTimer(ElectionTimeoutTimerName)

    electionDeadline = nextElectionDeadline()
    log.debug("Resetting election timeout: {}", electionDeadline)

    setTimer(ElectionTimeoutTimerName, ElectionTimeout, electionDeadline.timeLeft, repeat = false)

    electionDeadline
  }

  private def randomElectionTimeout(from: FiniteDuration, to: FiniteDuration): FiniteDuration = {
    val fromMs = from.toMillis
    val toMs = to.toMillis
    require(toMs > fromMs, s"to ($to) must be greater than from ($from) in order to create valid election timeout.")

    (fromMs + ThreadLocalRandom.current().nextInt(toMs.toInt - fromMs.toInt)).millis
  }

  // named state changes --------------------------------------------

  /** Start a new election */
  def beginElection(m: Meta) = {
    resetElectionDeadline()

    if (m.config.members.isEmpty) {
      // cluster is still discovering nodes, keep waiting
      goto(Follower) using m
    } else {
      goto(Candidate) using m.forNewElection forMax nextElectionDeadline().timeLeft
    }
  }

  /** Stop being the Leader */
  def stepDown(m: LeaderMeta) = {
    onIsNotLeader()
    goto(Follower) using m.forFollower
  }

  def onIsLeader(): Unit

  def onIsNotLeader(): Unit

  /** Stay in current state and reset the election timeout */
  def acceptHeartbeat() = {
    resetElectionDeadline()
    stay()
  }

  // end of named state changes -------------------------------------

  /** `true` if this follower is at `Term(2)`, yet the incoming term is `t > Term(3)` */
  def isInconsistentTerm(currentTerm: Term, term: Term): Boolean = term < currentTerm

  // sender aliases, for readability
  @inline def follower() = sender()
  @inline def candidate() = sender()
  @inline def leader() = sender()

  @inline def voter() = sender() // not explicitly a Raft role, but makes it nice to read
  // end of sender aliases

}







© 2015 - 2024 Weber Informatics LLC | Privacy Policy