HashRing.php 4.0 KB

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