HashRing.php 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. <?php
  2. namespace Predis\Distribution;
  3. class HashRing implements IDistributionStrategy
  4. {
  5. const DEFAULT_REPLICAS = 128;
  6. const DEFAULT_WEIGHT = 100;
  7. private $_nodes;
  8. private $_ring;
  9. private $_ringKeys;
  10. private $_ringKeysCount;
  11. private $_replicas;
  12. public function __construct($replicas = self::DEFAULT_REPLICAS)
  13. {
  14. $this->_replicas = $replicas;
  15. $this->_nodes = array();
  16. }
  17. public function add($node, $weight = null)
  18. {
  19. // In case of collisions in the hashes of the nodes, the node added
  20. // last wins, thus the order in which nodes are added is significant.
  21. $this->_nodes[] = array('object' => $node, 'weight' => (int) $weight ?: $this::DEFAULT_WEIGHT);
  22. $this->reset();
  23. }
  24. public function remove($node)
  25. {
  26. // A node is removed by resetting the ring so that it's recreated from
  27. // scratch, in order to reassign possible hashes with collisions to the
  28. // right node according to the order in which they were added in the
  29. // first place.
  30. for ($i = 0; $i < count($this->_nodes); ++$i) {
  31. if ($this->_nodes[$i]['object'] === $node) {
  32. array_splice($this->_nodes, $i, 1);
  33. $this->reset();
  34. break;
  35. }
  36. }
  37. }
  38. private function reset()
  39. {
  40. unset(
  41. $this->_ring,
  42. $this->_ringKeys,
  43. $this->_ringKeysCount
  44. );
  45. }
  46. private function isInitialized()
  47. {
  48. return isset($this->_ringKeys);
  49. }
  50. private function computeTotalWeight()
  51. {
  52. $totalWeight = 0;
  53. foreach ($this->_nodes as $node) {
  54. $totalWeight += $node['weight'];
  55. }
  56. return $totalWeight;
  57. }
  58. private function initialize()
  59. {
  60. if ($this->isInitialized()) {
  61. return;
  62. }
  63. if (count($this->_nodes) === 0) {
  64. throw new EmptyRingException('Cannot initialize empty hashring');
  65. }
  66. $this->_ring = array();
  67. $totalWeight = $this->computeTotalWeight();
  68. $nodesCount = count($this->_nodes);
  69. foreach ($this->_nodes as $node) {
  70. $weightRatio = $node['weight'] / $totalWeight;
  71. $this->addNodeToRing($this->_ring, $node, $nodesCount, $this->_replicas, $weightRatio);
  72. }
  73. ksort($this->_ring, SORT_NUMERIC);
  74. $this->_ringKeys = array_keys($this->_ring);
  75. $this->_ringKeysCount = count($this->_ringKeys);
  76. }
  77. protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio)
  78. {
  79. $nodeObject = $node['object'];
  80. $nodeHash = $this->getNodeHash($nodeObject);
  81. $replicas = (int) round($weightRatio * $totalNodes * $replicas);
  82. for ($i = 0; $i < $replicas; $i++) {
  83. $key = crc32("$nodeHash:$i");
  84. $ring[$key] = $nodeObject;
  85. }
  86. }
  87. protected function getNodeHash($nodeObject)
  88. {
  89. return (string) $nodeObject;
  90. }
  91. public function generateKey($value)
  92. {
  93. return crc32($value);
  94. }
  95. public function get($key)
  96. {
  97. return $this->_ring[$this->getNodeKey($key)];
  98. }
  99. private function getNodeKey($key)
  100. {
  101. $this->initialize();
  102. $ringKeys = $this->_ringKeys;
  103. $upper = $this->_ringKeysCount - 1;
  104. $lower = 0;
  105. while ($lower <= $upper) {
  106. $index = ($lower + $upper) >> 1;
  107. $item = $ringKeys[$index];
  108. if ($item > $key) {
  109. $upper = $index - 1;
  110. }
  111. else if ($item < $key) {
  112. $lower = $index + 1;
  113. }
  114. else {
  115. return $item;
  116. }
  117. }
  118. return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->_ringKeysCount)];
  119. }
  120. protected function wrapAroundStrategy($upper, $lower, $ringKeysCount)
  121. {
  122. // Binary search for the last item in _ringkeys with a value less or
  123. // equal to the key. If no such item exists, return the last item.
  124. return $upper >= 0 ? $upper : $ringKeysCount - 1;
  125. }
  126. }