mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-03-03 02:17:01 +00:00
271 lines
6.7 KiB
PHP
271 lines
6.7 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Predis package.
|
|
*
|
|
* (c) Daniele Alessandri <suppakilla@gmail.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Predis\Cluster\Distributor;
|
|
|
|
use Predis\Cluster\Hash\HashGeneratorInterface;
|
|
|
|
/**
|
|
* This class implements an hashring-based distributor that uses the same
|
|
* algorithm of memcache to distribute keys in a cluster using client-side
|
|
* sharding.
|
|
*
|
|
* @author Daniele Alessandri <suppakilla@gmail.com>
|
|
* @author Lorenzo Castelli <lcastelli@gmail.com>
|
|
*/
|
|
class HashRing implements DistributorInterface, HashGeneratorInterface
|
|
{
|
|
const DEFAULT_REPLICAS = 128;
|
|
const DEFAULT_WEIGHT = 100;
|
|
|
|
private $ring;
|
|
private $ringKeys;
|
|
private $ringKeysCount;
|
|
private $replicas;
|
|
private $nodeHashCallback;
|
|
private $nodes = array();
|
|
|
|
/**
|
|
* @param int $replicas Number of replicas in the ring.
|
|
* @param mixed $nodeHashCallback Callback returning a string used to calculate the hash of nodes.
|
|
*/
|
|
public function __construct($replicas = self::DEFAULT_REPLICAS, $nodeHashCallback = null)
|
|
{
|
|
$this->replicas = $replicas;
|
|
$this->nodeHashCallback = $nodeHashCallback;
|
|
}
|
|
|
|
/**
|
|
* Adds a node to the ring with an optional weight.
|
|
*
|
|
* @param mixed $node Node object.
|
|
* @param int $weight Weight for the node.
|
|
*/
|
|
public function add($node, $weight = null)
|
|
{
|
|
// In case of collisions in the hashes of the nodes, the node added
|
|
// last wins, thus the order in which nodes are added is significant.
|
|
$this->nodes[] = array(
|
|
'object' => $node,
|
|
'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT,
|
|
);
|
|
|
|
$this->reset();
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function remove($node)
|
|
{
|
|
// A node is removed by resetting the ring so that it's recreated from
|
|
// scratch, in order to reassign possible hashes with collisions to the
|
|
// right node according to the order in which they were added in the
|
|
// first place.
|
|
for ($i = 0; $i < count($this->nodes); ++$i) {
|
|
if ($this->nodes[$i]['object'] === $node) {
|
|
array_splice($this->nodes, $i, 1);
|
|
$this->reset();
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the distributor.
|
|
*/
|
|
private function reset()
|
|
{
|
|
unset(
|
|
$this->ring,
|
|
$this->ringKeys,
|
|
$this->ringKeysCount
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the initialization status of the distributor.
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function isInitialized()
|
|
{
|
|
return isset($this->ringKeys);
|
|
}
|
|
|
|
/**
|
|
* Calculates the total weight of all the nodes in the distributor.
|
|
*
|
|
* @return int
|
|
*/
|
|
private function computeTotalWeight()
|
|
{
|
|
$totalWeight = 0;
|
|
|
|
foreach ($this->nodes as $node) {
|
|
$totalWeight += $node['weight'];
|
|
}
|
|
|
|
return $totalWeight;
|
|
}
|
|
|
|
/**
|
|
* Initializes the distributor.
|
|
*/
|
|
private function initialize()
|
|
{
|
|
if ($this->isInitialized()) {
|
|
return;
|
|
}
|
|
|
|
if (!$this->nodes) {
|
|
throw new EmptyRingException('Cannot initialize an empty hashring.');
|
|
}
|
|
|
|
$this->ring = array();
|
|
$totalWeight = $this->computeTotalWeight();
|
|
$nodesCount = count($this->nodes);
|
|
|
|
foreach ($this->nodes as $node) {
|
|
$weightRatio = $node['weight'] / $totalWeight;
|
|
$this->addNodeToRing($this->ring, $node, $nodesCount, $this->replicas, $weightRatio);
|
|
}
|
|
|
|
ksort($this->ring, SORT_NUMERIC);
|
|
$this->ringKeys = array_keys($this->ring);
|
|
$this->ringKeysCount = count($this->ringKeys);
|
|
}
|
|
|
|
/**
|
|
* Implements the logic needed to add a node to the hashring.
|
|
*
|
|
* @param array $ring Source hashring.
|
|
* @param mixed $node Node object to be added.
|
|
* @param int $totalNodes Total number of nodes.
|
|
* @param int $replicas Number of replicas in the ring.
|
|
* @param float $weightRatio Weight ratio for the node.
|
|
*/
|
|
protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
|
|
{
|
|
$nodeObject = $node['object'];
|
|
$nodeHash = $this->getNodeHash($nodeObject);
|
|
$replicas = (int) round($weightRatio * $totalNodes * $replicas);
|
|
|
|
for ($i = 0; $i < $replicas; ++$i) {
|
|
$key = crc32("$nodeHash:$i");
|
|
$ring[$key] = $nodeObject;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
protected function getNodeHash($nodeObject)
|
|
{
|
|
if (!isset($this->nodeHashCallback)) {
|
|
return (string) $nodeObject;
|
|
}
|
|
|
|
return call_user_func($this->nodeHashCallback, $nodeObject);
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function hash($value)
|
|
{
|
|
return crc32($value);
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getByHash($hash)
|
|
{
|
|
return $this->ring[$this->getSlot($hash)];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getBySlot($slot)
|
|
{
|
|
$this->initialize();
|
|
|
|
if (isset($this->ring[$slot])) {
|
|
return $this->ring[$slot];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getSlot($hash)
|
|
{
|
|
$this->initialize();
|
|
|
|
$ringKeys = $this->ringKeys;
|
|
$upper = $this->ringKeysCount - 1;
|
|
$lower = 0;
|
|
|
|
while ($lower <= $upper) {
|
|
$index = ($lower + $upper) >> 1;
|
|
$item = $ringKeys[$index];
|
|
|
|
if ($item > $hash) {
|
|
$upper = $index - 1;
|
|
} elseif ($item < $hash) {
|
|
$lower = $index + 1;
|
|
} else {
|
|
return $item;
|
|
}
|
|
}
|
|
|
|
return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->ringKeysCount)];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function get($value)
|
|
{
|
|
$hash = $this->hash($value);
|
|
$node = $this->getByHash($hash);
|
|
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* Implements a strategy to deal with wrap-around errors during binary searches.
|
|
*
|
|
* @param int $upper
|
|
* @param int $lower
|
|
* @param int $ringKeysCount
|
|
*
|
|
* @return int
|
|
*/
|
|
protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
|
|
{
|
|
// Binary search for the last item in ringkeys with a value less or
|
|
// equal to the key. If no such item exists, return the last item.
|
|
return $upper >= 0 ? $upper : $ringKeysCount - 1;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getHashGenerator()
|
|
{
|
|
return $this;
|
|
}
|
|
}
|