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

com.teambytes.inflatable.raft.model.ReplicatedLog.scala Maven / Gradle / Ivy

The newest version!
package com.teambytes.inflatable.raft.model

import akka.actor.ActorRef
import scala.annotation.switch

/**
 * @param defaultBatchSize number of commands that can be sent together in one [[com.teambytes.inflatable.raft.protocol.RaftProtocol.AppendEntries]] message.
 */
private[inflatable] case class ReplicatedLog[Command](
  private[raft] val entries: List[Entry[Command]],
  committedIndex: Int,
  defaultBatchSize: Int) {

  def length = entries.length

  def commands: List[Command] = entries.map(_.command)

  /**
   * Performs the "consistency check", which checks if the data that we just got from the
   */
  def containsMatchingEntry(otherPrevTerm: Term, otherPrevIndex: Int): Boolean =
    (otherPrevTerm == Term(0) && otherPrevIndex == 0) || (lastTerm == otherPrevTerm && lastIndex == otherPrevIndex)

  // log state
  def lastTerm  = entries.lastOption map { _.term } getOrElse Term(0)
  def lastIndex = entries.lastOption map { _.index } getOrElse 0

  def prevIndex = (lastIndex: @switch) match {
    case 0 => 0 // special handling of initial case, we don't go into negative indexes
    case n => n - 1
  }
  def prevTerm  = if (entries.size < 2) Term(0) else entries.dropRight(1).last.term

  /**
   * Determines index of the next Entry that will be inserted into this log.
   * Handles edge cases, use this instead of +1'ing manualy.
   */
  def nextIndex = entries.size

  // log actions
  def commit(n: Int): ReplicatedLog[Command] =
    copy(committedIndex = n)

  def append(entry: Entry[Command], take: Int = entries.length): ReplicatedLog[Command] =
    append(List(entry), take)

  def append(entriesToAppend: Seq[Entry[Command]], take: Int): ReplicatedLog[Command] =
    copy(entries = entries.take(take) ++ entriesToAppend)

  def +(newEntry: Entry[Command]): ReplicatedLog[Command] =
    append(List(newEntry), entries.size)

  def putWithDroppingInconsistent(replicatedEntry: Entry[Command]): ReplicatedLog[Command] = {
    val replicatedIndex = replicatedEntry.index
    if (entries.isDefinedAt(replicatedIndex)) { // must now use index since we can have snapshots
      val localEntry = entries(replicatedIndex)

      if (localEntry == replicatedEntry) this // we're consistent with the replicated log
      else copy(entries = entries.slice(0, replicatedIndex) :+ replicatedEntry) // dropping everything until the entry that does not match
    } else {
      // nothing to drop
      this
    }
  }

  def compactedWith(snapshot: RaftSnapshot): ReplicatedLog[Command] = {
    val snapshotHasLaterTerm = snapshot.meta.lastIncludedTerm > lastTerm
    val snapshotHasLaterIndex = snapshot.meta.lastIncludedIndex > lastIndex

    if (snapshotHasLaterTerm && snapshotHasLaterIndex) {
      // total substitution, snapshot is from the future = replace all we had
      copy(entries = snapshot.toEntrySingleList)
    } else {
      val entriesWhereSnapshotteEntriesDropped = entries dropWhile { e => e.index <= snapshot.meta.lastIncludedIndex }
      copy(entries = snapshot.toEntry[Command] :: entriesWhereSnapshotteEntriesDropped)
    }
  }

  // log views

  def apply(idx: Int): Entry[Command] = entries(idx)

  /** @param fromIncluding index from which to start the slice (excluding the entry at that index) */
  def entriesBatchFrom(fromIncluding: Int, howMany: Int = 5): List[Entry[Command]] = {
    val toSend = entries.slice(fromIncluding, fromIncluding + howMany)
    toSend.headOption match {
      case Some(head) =>
        val batchTerm = head.term
        toSend.takeWhile(_.term == batchTerm) // we only batch commands grouped by their term

      case None =>
        List.empty
    }
  }

  def between(fromIndex: Int, toIndex: Int): List[Entry[Command]] =
    entries.slice(fromIndex + 1, toIndex + 1)

  def firstIndexInTerm(term: Term): Int = term.termNr match {
    case 0 => 0
    case 1 => 0
    case _ => entries.zipWithIndex find { case (e, i) => e.term == term } map { _._2 } getOrElse 0
  }

  def termAt(index: Int) =
    if (index <= 0) Term(0)
    else entries.find(_.index == index).getOrElse(throw new RuntimeException(s"Unable to find log entry at index $index")).term

  def committedEntries = entries.slice(0, committedIndex)

  def notCommittedEntries = entries.slice(committedIndex + 1, entries.length)
}

private[inflatable] class EmptyReplicatedLog[T](defaultBatchSize: Int) extends ReplicatedLog[T](List.empty, -1, defaultBatchSize) {
  override def lastTerm = Term(0)
  override def lastIndex = 0
}

private[inflatable] object ReplicatedLog {
  def empty[T](defaultBatchSize: Int): ReplicatedLog[T] = new EmptyReplicatedLog[T](defaultBatchSize)
}

private[inflatable] case class Entry[T](
  command: T,
  term: Term,
  index: Int,
  client: Option[ActorRef] = None
) {

  def isSnapshot = false

  def prevTerm = term.prev

  def prevIndex = index - 1 match {
    case -1 => 0
    case n  => n
  }
}

private[inflatable] trait SnapshotEntry {
  this: Entry[_] =>
  def data: Any = command

  override def isSnapshot = true
  override def toString = "Snapshot" + super.toString
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy