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

in.ashwanthkumar.suuchi.partitioner.ConsistentHashRing.scala Maven / Gradle / Ivy

The newest version!
package in.ashwanthkumar.suuchi.partitioner

import java.util

import in.ashwanthkumar.suuchi.cluster.MemberAddress
import in.ashwanthkumar.suuchi.utils.ByteArrayUtils

import scala.annotation.tailrec
import scala.collection.mutable

case class VNode(node: MemberAddress, nodeReplicaId: Int) {
  def key = node.host + "_" + node.port + "_" + nodeReplicaId
}

case class TokenRange(start: Int, end: Int, node: VNode) {
  def member = node.node
}

object RingState {

  /**
   * Check if `key` falls within the given range using the `hashFn`
   *
   * @param key        Key to check for
   * @param tokenRange TokenRange to check against
   * @param hashFn     HashFunction used in CHRing
   * @return true if he key falls within the range
   *         false otherwise
   */
  def contains(key: Array[Byte], tokenRange: TokenRange, hashFn: Hash): Boolean =
    contains(key, tokenRange.start, tokenRange.end, hashFn)

  /**
   * Check if `key` falls within the given range using the `hashFn`
   *
   * @param key    Key to check for
   * @param start  Start range of the Token
   * @param end    Last end of the Token
   * @param hashFn HashFunction used in CHRing
   * @return true if he key falls within the range
   *         false otherwise
   */
  def contains(key: Array[Byte], start: Int, end: Int, hashFn: Hash): Boolean = {
    ByteArrayUtils.isHashKeyWithinRange(start, end, key, hashFn)
  }

  def find(key: Array[Byte], ringState: RingState, hashFn: Hash): Option[TokenRange] = {
    ringState.ranges.find(r => contains(key, r, hashFn))
  }
}

case class RingState(private[partitioner] val lastKnown: Int, ranges: List[TokenRange]) {
  def byNodes = ranges.groupBy(_.node.node)

  def withReplication(replicationFactor: Int) =
    pick(ranges.length, replicationFactor, ranges ::: ranges, Map())

  @tailrec
  private final def pick(
      remaining: Int,
      replicationFactor: Int,
      ranges: List[TokenRange],
      result: Map[TokenRange, List[MemberAddress]]): Map[TokenRange, List[MemberAddress]] = {
    if (remaining == 0) result
    else {
      val replicas = ranges.map(_.member).distinct.take(replicationFactor)
      val tokens   = Map(ranges.head -> replicas)
      pick(remaining - 1, replicationFactor, ranges.tail, result ++ tokens)
    }
  }
}

// Ref - https://git.io/vPOP5
class ConsistentHashRing(hashFn: Hash, partitionsPerNode: Int, replicationFactor: Int = 2) {
  val sortedMap = new util.TreeMap[Integer, VNode]()

  // when looking for n unique nodes, give up after a streak of MAX_DUPES
  // duplicates
  val MAX_DUPES = 10

  def init(nodes: List[MemberAddress]) = {
    nodes.foreach(add)
    this
  }

  private def hash(vnode: VNode): Int = hashFn.hash(vnode.key.getBytes)

  def add(node: MemberAddress) = {
    (1 to partitionsPerNode).map(i => VNode(node, i)).foreach { vnode =>
      sortedMap.put(hash(vnode), vnode)
    }
    this
  }

  def remove(node: MemberAddress) = {
    (1 to partitionsPerNode).map(i => VNode(node, i)).foreach { vnode =>
      sortedMap.remove(hash(vnode))
    }
    this
  }

  def find(key: Array[Byte]): Option[MemberAddress] = {
    find(key, 1).headOption
  }

  /**
   * This returns the closest n nodes in order for the object. There may be
   * duplicates.
   */
  def find(key: Array[Byte], n: Int) = {
    if (sortedMap.isEmpty) Nil
    else {
      val (_, nodes) = (0 until n).foldLeft((hashFn.hash(key), List.empty[MemberAddress])) {
        case ((hash, members), idx) =>
          val (newHash, candidate) = findCandidate(hash)
          (newHash + 1, candidate :: members)
      }
      nodes.reverse
    }
  }

  /**
   * This returns the closest n nodes in order for the object. There is extra
   * code that forces the node values to be unique.
   *
   * This will return a list that has all the nodes (and is smaller than n) if n
   * > number of nodes.
   */
  def findUnique(key: Array[Byte], n: Int) = {
    if (sortedMap.isEmpty) Nil
    else {
      var duped       = 0
      var hashIdx     = hashFn.hash(key)
      val uniqueNodes = mutable.MutableList[MemberAddress]()
      var index       = 0
      while (index < n) {
        val (newHash, candidate) = findCandidate(hashIdx)
        hashIdx = newHash
        if (!uniqueNodes.contains(candidate)) {
          duped = 0
          uniqueNodes += candidate
        } else {
          duped += 1
          index -= 1 // try again
          if (duped > MAX_DUPES) {
            index += 1; // we've been duped too many times, just skip to next, returning
            // fewer than n
          }
        }

        // was a hit so we increment and loop to find the next node in the circle
        hashIdx += 1
        index += 1
      }
      uniqueNodes.toList
    }
  }

  /**
   * Represent the ConsistentHashRing as [[RingState]] which is more easier to work with in terms of Ranges that each node manages.
   *
   * @return  RingState
   */
  def ringState = {
    import scala.collection.JavaConversions._

    val firstToken = sortedMap.firstKey()
    val tokenRings = sortedMap.keysIterator.drop(1).foldLeft(RingState(firstToken, Nil)) {
      (state, token) =>
        RingState(
          token,
          ranges = TokenRange(state.lastKnown, token - 1, sortedMap.get(state.lastKnown)) :: state.ranges)
    }
    RingState(Int.MaxValue,
              ranges =
                (TokenRange(tokenRings.lastKnown,
                            firstToken - 1,
                            sortedMap.get(tokenRings.lastKnown)) :: tokenRings.ranges).reverse)
  }

  private[partitioner] def findCandidate(hash: Integer) = {
    if (sortedMap.containsKey(hash)) {
      hash -> sortedMap.get(hash).node
    } else {
      val tailMap = sortedMap.tailMap(hash)
      val newHash = if (tailMap.isEmpty) sortedMap.firstKey() else tailMap.firstKey()
      newHash -> sortedMap.get(newHash).node
    }
  }

  // USED ONLY FOR TESTS
  private[partitioner] def nodes = sortedMap.values()
}

object ConsistentHashRing {
  def apply(hashFn: Hash, nodes: List[MemberAddress], partitionsPerNode: Int): ConsistentHashRing =
    new ConsistentHashRing(SuuchiHash, partitionsPerNode).init(nodes)

  def apply(nodes: List[MemberAddress], partitionsPerNode: Int): ConsistentHashRing =
    apply(SuuchiHash, nodes, partitionsPerNode)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy