_numberOfReplicas = $numberOfReplicas; $this->addNodes($nodes, $weight); } /** * Get the primary node for $key. * * @param string $key The key to look up. * * @param string The primary node for $key. */ public function get($key) { $nodes = $this->getNodes($key, 1); if (!$nodes) { throw new Exception('No nodes found'); } return $nodes[0]; } /** * Get an ordered list of nodes for $key. * * @param string $key The key to look up. * @param integer $count The number of nodes to look up. * * @return array An ordered array of nodes. */ public function getNodes($key, $count = 5) { // Degenerate cases if ($this->_nodeCount < $count) { throw new Exception('Not enough nodes (have ' . $this->_nodeCount . ', ' . $count . ' requested)'); } if ($this->_nodeCount == 0) { return array(); } // Simple case if ($this->_nodeCount == 1) { return array($this->_nodes[0]['n']); } $hash = $this->hash(serialize($key)); // Find the first point on the circle greater than $hash by binary search. $low = 0; $high = $this->_pointCount - 1; $index = null; while (true) { $mid = (int)(($low + $high) / 2); if ($mid == $this->_pointCount) { $index = 0; break; } $midval = $this->_pointMap[$mid]; $midval1 = ($mid == 0) ? 0 : $this->_pointMap[$mid - 1]; if ($midval1 < $hash && $hash <= $midval) { $index = $mid; break; } if ($midval > $hash) { $high = $mid - 1; } else { $low = $mid + 1; } if ($low > $high) { $index = 0; break; } } $nodes = array(); while (count($nodes) < $count) { $nodeIndex = $this->_pointMap[$index++ % $this->_pointCount]; $nodes[$nodeIndex] = $this->_nodes[$this->_circle[$nodeIndex]]['n']; } return array_values($nodes); } /** * Add $node with weight $weight * * @param mixed $node */ public function add($node, $weight = 1) { // Delegate to addNodes so that the circle is only regenerated once when // adding multiple nodes. $this->addNodes(array($node), $weight); } /** * Add multiple nodes to the hash with the same weight. * * @param array $nodes An array of nodes. * @param integer $weight The weight to add the nodes with. */ public function addNodes($nodes, $weight = 1) { foreach ($nodes as $node) { $this->_nodes[] = array('n' => $node, 'w' => $weight); $this->_nodeCount++; $nodeIndex = $this->_nodeCount - 1; $nodeString = serialize($node); $numberOfReplicas = (int)($weight * $this->_numberOfReplicas); for ($i = 0; $i < $numberOfReplicas; $i++) { $this->_circle[$this->hash($nodeString . $i)] = $nodeIndex; } } $this->_updateCircle(); } /** * Remove $node from the hash. * * @param mixed $node */ public function remove($node) { $nodeIndex = null; $nodeString = serialize($node); // Search for the node in the node list foreach (array_keys($this->_nodes) as $i) { if ($this->_nodes[$i]['n'] === $node) { $nodeIndex = $i; break; } } if (is_null($nodeIndex)) { throw new InvalidArgumentException('Node was not in the hash'); } // Remove all points from the circle $numberOfReplicas = (int)($this->_nodes[$nodeIndex]['w'] * $this->_numberOfReplicas); for ($i = 0; $i < $numberOfReplicas; $i++) { unset($this->_circle[$this->hash($nodeString . $i)]); } $this->_updateCircle(); // Unset the node from the node list unset($this->_nodes[$nodeIndex]); $this->_nodeCount--; } /** * Expose the hash function for testing, probing, and extension. * * @param string $key * * @return string Hash value */ public function hash($key) { return 'h' . substr(hash('md5', $key), 0, 8); } /** * Maintain the circle and arrays of points. */ protected function _updateCircle() { // Sort the circle ksort($this->_circle); // Now that the hashes are sorted, generate numeric indices into the // circle. $this->_pointMap = array_keys($this->_circle); $this->_pointCount = count($this->_pointMap); } }