HashRing.php 6.6 KB

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