HashRing.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <?php
  2. /*
  3. * This file is part of the Predis package.
  4. *
  5. * (c) Daniele Alessandri <suppakilla@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Predis\Cluster\Distribution;
  11. use Predis\Cluster\Hash\HashGeneratorInterface;
  12. /**
  13. * This class implements an hashring-based distributor that uses the same
  14. * algorithm of memcache to distribute keys in a cluster using client-side
  15. * sharding.
  16. *
  17. * @author Daniele Alessandri <suppakilla@gmail.com>
  18. * @author Lorenzo Castelli <lcastelli@gmail.com>
  19. */
  20. class HashRing implements DistributionStrategyInterface, HashGeneratorInterface
  21. {
  22. const DEFAULT_REPLICAS = 128;
  23. const DEFAULT_WEIGHT = 100;
  24. private $nodes;
  25. private $ring;
  26. private $ringKeys;
  27. private $ringKeysCount;
  28. private $replicas;
  29. /**
  30. * @param int $replicas Number of replicas in the ring.
  31. */
  32. public function __construct($replicas = self::DEFAULT_REPLICAS)
  33. {
  34. $this->replicas = $replicas;
  35. $this->nodes = array();
  36. }
  37. /**
  38. * Adds a node to the ring with an optional weight.
  39. *
  40. * @param mixed $node Node object.
  41. * @param int $weight Weight for the node.
  42. */
  43. public function add($node, $weight = null)
  44. {
  45. // In case of collisions in the hashes of the nodes, the node added
  46. // last wins, thus the order in which nodes are added is significant.
  47. $this->nodes[] = array('object' => $node, 'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT);
  48. $this->reset();
  49. }
  50. /**
  51. * {@inheritdoc}
  52. */
  53. public function remove($node)
  54. {
  55. // A node is removed by resetting the ring so that it's recreated from
  56. // scratch, in order to reassign possible hashes with collisions to the
  57. // right node according to the order in which they were added in the
  58. // first place.
  59. for ($i = 0; $i < count($this->nodes); ++$i) {
  60. if ($this->nodes[$i]['object'] === $node) {
  61. array_splice($this->nodes, $i, 1);
  62. $this->reset();
  63. break;
  64. }
  65. }
  66. }
  67. /**
  68. * Resets the distributor.
  69. */
  70. private function reset()
  71. {
  72. unset(
  73. $this->ring,
  74. $this->ringKeys,
  75. $this->ringKeysCount
  76. );
  77. }
  78. /**
  79. * Returns the initialization status of the distributor.
  80. *
  81. * @return Boolean
  82. */
  83. private function isInitialized()
  84. {
  85. return isset($this->ringKeys);
  86. }
  87. /**
  88. * Calculates the total weight of all the nodes in the distributor.
  89. *
  90. * @return int
  91. */
  92. private function computeTotalWeight()
  93. {
  94. $totalWeight = 0;
  95. foreach ($this->nodes as $node) {
  96. $totalWeight += $node['weight'];
  97. }
  98. return $totalWeight;
  99. }
  100. /**
  101. * Initializes the distributor.
  102. */
  103. private function initialize()
  104. {
  105. if ($this->isInitialized()) {
  106. return;
  107. }
  108. if (count($this->nodes) === 0) {
  109. throw new EmptyRingException('Cannot initialize empty hashring');
  110. }
  111. $this->ring = array();
  112. $totalWeight = $this->computeTotalWeight();
  113. $nodesCount = count($this->nodes);
  114. foreach ($this->nodes as $node) {
  115. $weightRatio = $node['weight'] / $totalWeight;
  116. $this->addNodeToRing($this->ring, $node, $nodesCount, $this->replicas, $weightRatio);
  117. }
  118. ksort($this->ring, SORT_NUMERIC);
  119. $this->ringKeys = array_keys($this->ring);
  120. $this->ringKeysCount = count($this->ringKeys);
  121. }
  122. /**
  123. * Implements the logic needed to add a node to the hashring.
  124. *
  125. * @param array $ring Source hashring.
  126. * @param mixed $node Node object to be added.
  127. * @param int $totalNodes Total number of nodes.
  128. * @param int $replicas Number of replicas in the ring.
  129. * @param float $weightRatio Weight ratio for the node.
  130. */
  131. protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
  132. {
  133. $nodeObject = $node['object'];
  134. $nodeHash = $this->getNodeHash($nodeObject);
  135. $replicas = (int) round($weightRatio * $totalNodes * $replicas);
  136. for ($i = 0; $i < $replicas; $i++) {
  137. $key = crc32("$nodeHash:$i");
  138. $ring[$key] = $nodeObject;
  139. }
  140. }
  141. /**
  142. * {@inheritdoc}
  143. */
  144. protected function getNodeHash($nodeObject)
  145. {
  146. return (string) $nodeObject;
  147. }
  148. /**
  149. * Calculates the hash for the specified value.
  150. *
  151. * @param string $value Input value.
  152. * @return int
  153. */
  154. public function hash($value)
  155. {
  156. return crc32($value);
  157. }
  158. /**
  159. * {@inheritdoc}
  160. */
  161. public function get($key)
  162. {
  163. return $this->ring[$this->getNodeKey($key)];
  164. }
  165. /**
  166. * Calculates the corrisponding key of a node distributed in the hashring.
  167. *
  168. * @param int $key Computed hash of a key.
  169. * @return int
  170. */
  171. private function getNodeKey($key)
  172. {
  173. $this->initialize();
  174. $ringKeys = $this->ringKeys;
  175. $upper = $this->ringKeysCount - 1;
  176. $lower = 0;
  177. while ($lower <= $upper) {
  178. $index = ($lower + $upper) >> 1;
  179. $item = $ringKeys[$index];
  180. if ($item > $key) {
  181. $upper = $index - 1;
  182. } else if ($item < $key) {
  183. $lower = $index + 1;
  184. } else {
  185. return $item;
  186. }
  187. }
  188. return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->ringKeysCount)];
  189. }
  190. /**
  191. * Implements a strategy to deal with wrap-around errors during binary searches.
  192. *
  193. * @param int $upper
  194. * @param int $lower
  195. * @param int $ringKeysCount
  196. * @return int
  197. */
  198. protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
  199. {
  200. // Binary search for the last item in ringkeys with a value less or
  201. // equal to the key. If no such item exists, return the last item.
  202. return $upper >= 0 ? $upper : $ringKeysCount - 1;
  203. }
  204. /**
  205. * {@inheritdoc}
  206. */
  207. public function getHashGenerator()
  208. {
  209. return $this;
  210. }
  211. }