From 7bf1f6eb29de182b17396924fc135ed76af64beb Mon Sep 17 00:00:00 2001 From: messikiller Date: Thu, 20 Mar 2025 00:24:53 +0800 Subject: [PATCH] optimize redis cluster slotmap with compact slot range object (#1493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: heqiming Co-authored-by: Till Krüss Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- src/Cluster/NullSlotRange.php | 40 ++ src/Cluster/SimpleSlotMap.php | 209 +++++++++ src/Cluster/SlotMap.php | 242 ++++++++++- src/Cluster/SlotRange.php | 145 +++++++ tests/Predis/Cluster/SimpleSlotMapTest.php | 473 +++++++++++++++++++++ tests/Predis/Cluster/SlotRangeTest.php | 134 ++++++ 6 files changed, 1226 insertions(+), 17 deletions(-) create mode 100644 src/Cluster/NullSlotRange.php create mode 100644 src/Cluster/SimpleSlotMap.php create mode 100644 src/Cluster/SlotRange.php create mode 100644 tests/Predis/Cluster/SimpleSlotMapTest.php create mode 100644 tests/Predis/Cluster/SlotRangeTest.php diff --git a/src/Cluster/NullSlotRange.php b/src/Cluster/NullSlotRange.php new file mode 100644 index 00000000..d9561e4b --- /dev/null +++ b/src/Cluster/NullSlotRange.php @@ -0,0 +1,40 @@ += 0x0000 && $slot <= 0x3FFF; + } + + /** + * Checks if the given slot range is valid. + * + * @param int $first Initial slot of the range. + * @param int $last Last slot of the range. + * + * @return bool + */ + public static function isValidRange($first, $last) + { + return $first >= 0x0000 && $first <= 0x3FFF && $last >= 0x0000 && $last <= 0x3FFF && $first <= $last; + } + + /** + * Resets the slot map. + */ + public function reset() + { + $this->slots = []; + } + + /** + * Checks if the slot map is empty. + * + * @return bool + */ + public function isEmpty() + { + return empty($this->slots); + } + + /** + * Returns the current slot map as a dictionary of $slot => $node. + * + * The order of the slots in the dictionary is not guaranteed. + * + * @return array + */ + public function toArray() + { + return $this->slots; + } + + /** + * Returns the list of unique nodes in the slot map. + * + * @return array + */ + public function getNodes() + { + return array_keys(array_flip($this->slots)); + } + + /** + * Assigns the specified slot range to a node. + * + * @param int $first Initial slot of the range. + * @param int $last Last slot of the range. + * @param NodeConnectionInterface|string $connection ID or connection instance. + * + * @throws OutOfBoundsException + */ + public function setSlots($first, $last, $connection) + { + if (!static::isValidRange($first, $last)) { + throw new OutOfBoundsException("Invalid slot range $first-$last for `$connection`"); + } + + $this->slots += array_fill($first, $last - $first + 1, (string) $connection); + } + + /** + * Returns the specified slot range. + * + * @param int $first Initial slot of the range. + * @param int $last Last slot of the range. + * + * @return array + */ + public function getSlots($first, $last) + { + if (!static::isValidRange($first, $last)) { + throw new OutOfBoundsException("Invalid slot range $first-$last"); + } + + return array_intersect_key($this->slots, array_fill($first, $last - $first + 1, null)); + } + + /** + * Checks if the specified slot is assigned. + * + * @param int $slot Slot index. + * + * @return bool + */ + #[ReturnTypeWillChange] + public function offsetExists($slot) + { + return isset($this->slots[$slot]); + } + + /** + * Returns the node assigned to the specified slot. + * + * @param int $slot Slot index. + * + * @return string|null + */ + #[ReturnTypeWillChange] + public function offsetGet($slot) + { + return $this->slots[$slot] ?? null; + } + + /** + * Assigns the specified slot to a node. + * + * @param int $slot Slot index. + * @param NodeConnectionInterface|string $connection ID or connection instance. + * + * @return void + */ + #[ReturnTypeWillChange] + public function offsetSet($slot, $connection) + { + if (!static::isValid($slot)) { + throw new OutOfBoundsException("Invalid slot $slot for `$connection`"); + } + + $this->slots[(int) $slot] = (string) $connection; + } + + /** + * Returns the node assigned to the specified slot. + * + * @param int $slot Slot index. + * + * @return void + */ + #[ReturnTypeWillChange] + public function offsetUnset($slot) + { + unset($this->slots[$slot]); + } + + /** + * Returns the current number of assigned slots. + * + * @return int + */ + #[ReturnTypeWillChange] + public function count() + { + return count($this->slots); + } + + /** + * Returns an iterator over the slot map. + * + * @return Traversable + */ + #[ReturnTypeWillChange] + public function getIterator() + { + return new ArrayIterator($this->slots); + } +} diff --git a/src/Cluster/SlotMap.php b/src/Cluster/SlotMap.php index 17b192ed..5a2b859e 100644 --- a/src/Cluster/SlotMap.php +++ b/src/Cluster/SlotMap.php @@ -22,11 +22,16 @@ use ReturnTypeWillChange; use Traversable; /** - * Slot map for redis-cluster. + * Compact slot map for redis-cluster. */ class SlotMap implements ArrayAccess, IteratorAggregate, Countable { - private $slots = []; + /** + * Slot ranges list. + * + * @var SlotRange[] + */ + private $slotRanges = []; /** * Checks if the given slot is valid. @@ -37,7 +42,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable */ public static function isValid($slot) { - return $slot >= 0x0000 && $slot <= 0x3FFF; + return $slot >= 0 && $slot <= SlotRange::MAX_SLOTS; } /** @@ -50,7 +55,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable */ public static function isValidRange($first, $last) { - return $first >= 0x0000 && $first <= 0x3FFF && $last >= 0x0000 && $last <= 0x3FFF && $first <= $last; + return SlotRange::isValidRange($first, $last); } /** @@ -58,7 +63,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable */ public function reset() { - $this->slots = []; + $this->slotRanges = []; } /** @@ -68,7 +73,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable */ public function isEmpty() { - return empty($this->slots); + return empty($this->slotRanges); } /** @@ -80,7 +85,13 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable */ public function toArray() { - return $this->slots; + return array_reduce( + $this->slotRanges, + function ($carry, $slotRange) { + return $carry + $slotRange->toArray(); + }, + [] + ); } /** @@ -90,7 +101,22 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable */ public function getNodes() { - return array_keys(array_flip($this->slots)); + return array_unique(array_map( + function ($slotRange) { + return $slotRange->getConnection(); + }, + $this->slotRanges + )); + } + + /** + * Returns the list of slot ranges. + * + * @return SlotRange[] + */ + public function getSlotRanges() + { + return $this->slotRanges; } /** @@ -108,7 +134,31 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable throw new OutOfBoundsException("Invalid slot range $first-$last for `$connection`"); } - $this->slots += array_fill($first, $last - $first + 1, (string) $connection); + $targetSlotRange = new SlotRange($first, $last, (string) $connection); + + // Get gaps of slot ranges list. + $gaps = $this->getGaps($this->slotRanges); + + $results = $this->slotRanges; + + foreach ($gaps as $gap) { + if (!$gap->hasIntersectionWith($targetSlotRange)) { + continue; + } + + // Get intersection of the gap and target slot range. + $results[] = new SlotRange( + max($gap->getStart(), $targetSlotRange->getStart()), + min($gap->getEnd(), $targetSlotRange->getEnd()), + $targetSlotRange->getConnection() + ); + } + + $this->sortSlotRanges($results); + + $results = $this->compactSlotRanges($results); + + $this->slotRanges = $results; } /** @@ -117,7 +167,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable * @param int $first Initial slot of the range. * @param int $last Last slot of the range. * - * @return array + * @return array */ public function getSlots($first, $last) { @@ -125,7 +175,28 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable throw new OutOfBoundsException("Invalid slot range $first-$last"); } - return array_intersect_key($this->slots, array_fill($first, $last - $first + 1, null)); + $placeHolder = new NullSlotRange($first, $last); + + $intersections = []; + foreach ($this->slotRanges as $slotRange) { + if (!$placeHolder->hasIntersectionWith($slotRange)) { + continue; + } + + $intersections[] = new SlotRange( + max($placeHolder->getStart(), $slotRange->getStart()), + min($placeHolder->getEnd(), $slotRange->getEnd()), + $slotRange->getConnection() + ); + } + + return array_reduce( + $intersections, + function ($carry, $slotRange) { + return $carry + $slotRange->toArray(); + }, + [] + ); } /** @@ -138,7 +209,7 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable #[ReturnTypeWillChange] public function offsetExists($slot) { - return isset($this->slots[$slot]); + return $this->findRangeBySlot($slot) !== false; } /** @@ -151,7 +222,9 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable #[ReturnTypeWillChange] public function offsetGet($slot) { - return $this->slots[$slot] ?? null; + $found = $this->findRangeBySlot($slot); + + return $found ? $found->getConnection() : null; } /** @@ -169,7 +242,8 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable throw new OutOfBoundsException("Invalid slot $slot for `$connection`"); } - $this->slots[(int) $slot] = (string) $connection; + $this->offsetUnset($slot); + $this->setSlots($slot, $slot, $connection); } /** @@ -182,7 +256,26 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable #[ReturnTypeWillChange] public function offsetUnset($slot) { - unset($this->slots[$slot]); + if (!static::isValid($slot)) { + throw new OutOfBoundsException("Invalid slot $slot"); + } + + $results = []; + foreach ($this->slotRanges as $slotRange) { + if (!$slotRange->hasSlot($slot)) { + $results[] = $slotRange; + } + + if (static::isValidRange($slotRange->getStart(), $slot - 1)) { + $results[] = new SlotRange($slotRange->getStart(), $slot - 1, $slotRange->getConnection()); + } + + if (static::isValidRange($slot + 1, $slotRange->getEnd())) { + $results[] = new SlotRange($slot + 1, $slotRange->getEnd(), $slotRange->getConnection()); + } + } + + $this->slotRanges = $results; } /** @@ -193,7 +286,12 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable #[ReturnTypeWillChange] public function count() { - return count($this->slots); + return array_sum(array_map( + function ($slotRange) { + return $slotRange->count(); + }, + $this->slotRanges + )); } /** @@ -204,6 +302,116 @@ class SlotMap implements ArrayAccess, IteratorAggregate, Countable #[ReturnTypeWillChange] public function getIterator() { - return new ArrayIterator($this->slots); + return new ArrayIterator($this->toArray()); + } + + /** + * Find the slot range which contains the specific slot index. + * + * @param int $slot Slot index. + * + * @return SlotRange|false The slot range object or false if not found. + */ + protected function findRangeBySlot(int $slot) + { + foreach ($this->slotRanges as $slotRange) { + if ($slotRange->hasSlot($slot)) { + return $slotRange; + } + } + + return false; + } + + /** + * Get gaps between sorted slot ranges with NullSlotRange object. + * + * @param SlotRange[] $slotRanges + * + * @return SlotRange[] + */ + protected function getGaps(array $slotRanges) + { + if (empty($slotRanges)) { + return [ + new NullSlotRange(0, SlotRange::MAX_SLOTS), + ]; + } + $gaps = []; + $count = count($slotRanges); + $i = 0; + foreach ($slotRanges as $key => $slotRange) { + $start = $slotRange->getStart(); + $end = $slotRange->getEnd(); + if (static::isValidRange($i, $start - 1)) { + $gaps[] = new NullSlotRange($i, $start - 1); + } + + $i = $end + 1; + + if ($key === $count - 1) { + if (static::isValidRange($i, SlotRange::MAX_SLOTS)) { + $gaps[] = new NullSlotRange($i, SlotRange::MAX_SLOTS); + } + } + } + + return $gaps; + } + + /** + * Sort slot ranges by start index. + * + * @param SlotRange[] $slotRanges + * + * @return void + */ + protected function sortSlotRanges(array &$slotRanges) + { + usort( + $slotRanges, + function (SlotRange $a, SlotRange $b) { + if ($a->getStart() == $b->getStart()) { + return 0; + } + + return $a->getStart() < $b->getStart() ? -1 : 1; + } + ); + } + + /** + * Compact adjacent slot ranges with the same connection. + * + * @param SlotRange[] $slotRanges + * + * @return SlotRange[] + */ + protected function compactSlotRanges(array $slotRanges) + { + if (empty($slotRanges)) { + return []; + } + + $compacted = []; + $count = count($slotRanges); + $i = 0; + $carry = $slotRanges[0]; + while ($i < $count) { + $next = $slotRanges[$i + 1] ?? null; + if ( + !is_null($next) + && ($carry->getEnd() + 1) === $next->getStart() + && $carry->getConnection() === $next->getConnection() + ) { + $carry = new SlotRange($carry->getStart(), $next->getEnd(), $carry->getConnection()); + } else { + $compacted[] = $carry; + $carry = $next; + } + $i++; + } + + return array_values($compacted); } } diff --git a/src/Cluster/SlotRange.php b/src/Cluster/SlotRange.php new file mode 100644 index 00000000..95fdeab4 --- /dev/null +++ b/src/Cluster/SlotRange.php @@ -0,0 +1,145 @@ +start = $start; + $this->end = $end; + $this->connection = $connection; + } + + /** + * Checks if a slot range is valid. + * + * @param int $first + * @param int $last + * + * @return bool + */ + public static function isValidRange($first, $last) + { + return $first >= 0 && $first <= self::MAX_SLOTS && $last >= 0x0000 && $last <= self::MAX_SLOTS && $first <= $last; + } + + /** + * Returns the start slot index of this range. + * + * @return int + */ + public function getStart() + { + return $this->start; + } + + /** + * Returns the end slot index of this range. + * + * @return int + */ + public function getEnd() + { + return $this->end; + } + + /** + * Returns the connection to the server hosting this slot range. + * + * @return string + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Checks if the specific slot is contained in this range. + * + * @param int $slot + * + * @return bool + */ + public function hasSlot(int $slot) + { + return $this->start <= $slot && $this->end >= $slot; + } + + /** + * Returns an array of connection strings for each slot in this range. + * + * @return string[] + */ + public function toArray(): array + { + return array_fill($this->start, $this->end - $this->start + 1, $this->connection); + } + + /** + * Returns the number of slots in this range. + * + * @return int + */ + public function count(): int + { + return $this->end - $this->start + 1; + } + + /** + * Checks if this range has an intersection with the given slot range. + * + * @param SlotRange $slotRange + * + * @return bool + */ + public function hasIntersectionWith(SlotRange $slotRange): bool + { + return $this->start <= $slotRange->getEnd() && $this->end >= $slotRange->getStart(); + } +} diff --git a/tests/Predis/Cluster/SimpleSlotMapTest.php b/tests/Predis/Cluster/SimpleSlotMapTest.php new file mode 100644 index 00000000..95bfae34 --- /dev/null +++ b/tests/Predis/Cluster/SimpleSlotMapTest.php @@ -0,0 +1,473 @@ +assertTrue(SlotMap::isValid(0)); + $this->assertTrue(SlotMap::isValid(16383)); + + $this->assertTrue(SlotMap::isValid(5000)); + $this->assertTrue(SlotMap::isValid('5000')); + } + + /** + * @group disconnected + */ + public function testIsValidReturnsFalseOnInvalidSlot(): void + { + $this->assertFalse(SlotMap::isValid(-1)); + $this->assertFalse(SlotMap::isValid(16384)); + } + + /** + * @group disconnected + */ + public function testIsValidRangeReturnsTrueOnValidSlotRange(): void + { + $this->assertTrue(SlotMap::isValidRange(0, 16383)); + $this->assertTrue(SlotMap::isValidRange(2000, 2999)); + $this->assertTrue(SlotMap::isValidRange(3000, 3000)); + } + + /** + * @group disconnected + */ + public function testIsValidRangeReturnsFalseOnInvalidSlotRange(): void + { + $this->assertFalse(SlotMap::isValidRange(0, 16384)); + $this->assertFalse(SlotMap::isValidRange(-1, 16383)); + $this->assertFalse(SlotMap::isValidRange(-1, 16384)); + $this->assertFalse(SlotMap::isValidRange(2999, 2000)); + } + + /** + * @group disconnected + */ + public function testToArrayReturnsEmptyArrayOnEmptySlotMap(): void + { + $slotmap = new SlotMap(); + + $this->assertEmpty($slotmap->toArray()); + } + + /** + * @group disconnected + */ + public function testSetSlotsAssignsSpecifiedNodeToSlotRange(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + $slotmap->setSlots(5461, 10922, '127.0.0.1:6380'); + $slotmap->setSlots(10923, 16383, '127.0.0.1:6381'); + + $expectedMap = array_merge( + array_fill(0, 5461, '127.0.0.1:6379'), + array_fill(5461, 5462, '127.0.0.1:6380'), + array_fill(10923, 5461, '127.0.0.1:6381') + ); + + $this->assertSame($expectedMap, $slotmap->toArray()); + } + + /** + * @group disconnected + */ + public function testSetSlotsOverwritesSlotRange(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + $slotmap->setSlots(1000, 2000, '127.0.0.1:6380'); + + $expectedMap = + array_fill(0, 5461, '127.0.0.1:6379') + + array_fill(1000, 2000, '127.0.0.1:6380'); + + $this->assertSame($expectedMap, $slotmap->toArray()); + } + + /** + * @group disconnected + */ + public function testSetSlotsAssignsSingleSlotWhenFirstAndLastSlotMatch(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(10, 10, '127.0.0.1:6379'); + + $this->assertSame([10 => '127.0.0.1:6379'], $slotmap->toArray()); + } + + /** + * @group disconnected + */ + public function testSetSlotsCastsValueToString(): void + { + $slotmap = new SlotMap(); + + $connection = $this->getMockConnection(); + $connection + ->expects($this->once()) + ->method('__toString') + ->willReturn('127.0.0.1:6379'); + + $slotmap->setSlots(10, 10, $connection); + + $this->assertSame([10 => '127.0.0.1:6379'], $slotmap->toArray()); + } + + /** + * @group disconnected + */ + public function testSetSlotsThrowsExceptionOnInvalidSlotRange(): void + { + $this->expectException('OutOfBoundsException'); + $this->expectExceptionMessage('Invalid slot range 0-16384 for `127.0.0.1:6379`'); + + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 16384, '127.0.0.1:6379'); + } + + /** + * @group disconnected + */ + public function testGetSlotsReturnsEmptyArrayOnEmptySlotMap(): void + { + $slotmap = new SlotMap(); + + $this->assertEmpty($slotmap->getSlots(3, 11)); + } + + /** + * @group disconnected + */ + public function testGetSlotsReturnsDictionaryOfSlotsWithAssignedNodes(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5, '127.0.0.1:6379'); + $slotmap->setSlots(10, 13, '127.0.0.1:6380'); + + $expectedMap = [ + 3 => '127.0.0.1:6379', + 4 => '127.0.0.1:6379', + 5 => '127.0.0.1:6379', + 10 => '127.0.0.1:6380', + 11 => '127.0.0.1:6380', + ]; + + $this->assertSame($expectedMap, $slotmap->getSlots(3, 11)); + } + + /** + * @group disconnected + */ + public function testGetSlotsReturnsEmptyArrayOnEmptySlotRange(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5, '127.0.0.1:6379'); + $slotmap->setSlots(10, 13, '127.0.0.1:6380'); + + $this->assertEmpty($slotmap->getSlots(100, 200)); + } + + /** + * @group disconnected + */ + public function testGetSlotsThrowsExceptionOnInvalidSlotRange(): void + { + $this->expectException('OutOfBoundsException'); + $this->expectExceptionMessage('Invalid slot range 0-16384'); + + $slotmap = new SlotMap(); + + $slotmap->getSlots(0, 16384); + } + + /** + * @group disconnected + */ + public function testIsEmptyReturnsTrueOnEmptySlotMap(): void + { + $slotmap = new SlotMap(); + + $this->assertTrue($slotmap->isEmpty()); + } + + /** + * @group disconnected + */ + public function testIsEmptyReturnsFalseOnNonEmptySlotMap(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + + $this->assertFalse($slotmap->isEmpty()); + } + + /** + * @group disconnected + */ + public function testCountReturnsZeroOnEmptySlotMap(): void + { + $slotmap = new SlotMap(); + + $this->assertCount(0, $slotmap); + } + + /** + * @group disconnected + */ + public function testCountReturnsAssignedSlotsInSlotMap(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + $this->assertCount(5461, $slotmap); + + $slotmap->setSlots(5461, 10922, '127.0.0.1:6380'); + $this->assertCount(10923, $slotmap); + + $slotmap->setSlots(10923, 16383, '127.0.0.1:6381'); + $this->assertCount(16384, $slotmap); + } + + /** + * @group disconnected + */ + public function testResetEmptiesSlotMap(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + $slotmap->setSlots(5461, 10922, '127.0.0.1:6380'); + $slotmap->setSlots(10923, 16383, '127.0.0.1:6381'); + + $this->assertFalse($slotmap->isEmpty()); + + $slotmap->reset(); + + $this->assertTrue($slotmap->isEmpty()); + } + + /** + * @group disconnected + */ + public function testGetNodesReturnsEmptyArrayOnEmptySlotMap(): void + { + $slotmap = new SlotMap(); + + $this->assertEmpty($slotmap->getNodes()); + } + + /** + * @group disconnected + */ + public function testGetNodesReturnsArrayOfNodesInSlotMap(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + $slotmap->setSlots(5461, 10922, '127.0.0.1:6380'); + $slotmap->setSlots(10923, 16383, '127.0.0.1:6381'); + + $this->assertSame(['127.0.0.1:6379', '127.0.0.1:6380', '127.0.0.1:6381'], $slotmap->getNodes()); + } + + /** + * @group disconnected + */ + public function testOffsetExistsReturnsTrueOnAssignedSlot(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + + $this->assertTrue(isset($slotmap[0])); + $this->assertTrue(isset($slotmap[2000])); + } + + /** + * @group disconnected + */ + public function testOffsetExistsReturnsFalseOnAssignedSlot(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + + $this->assertFalse(isset($slotmap[6000])); + } + + /** + * @group disconnected + */ + public function testOffsetExistsReturnsFalseOnInvalidSlot(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + + $this->assertFalse(isset($slotmap[-100])); + $this->assertFalse(isset($slotmap[16384])); + } + + /** + * @group disconnected + */ + public function testOffsetGetReturnsNodeOfAssignedSlot(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + $slotmap->setSlots(5461, 10922, '127.0.0.1:6380'); + $slotmap->setSlots(10923, 16383, '127.0.0.1:6381'); + + $this->assertSame('127.0.0.1:6379', $slotmap[0]); + $this->assertSame('127.0.0.1:6380', $slotmap[5461]); + $this->assertSame('127.0.0.1:6381', $slotmap[10923]); + } + + /** + * @group disconnected + */ + public function testOffsetGetReturnsNullOnUnassignedSlot(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + + $this->assertNull($slotmap[5461]); + } + + /** + * @group disconnected + */ + public function testOffsetGetReturnsNullOnInvalidSlot(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + + $this->assertNull($slotmap[-100]); + $this->assertNull($slotmap[16384]); + } + + /** + * @group disconnected + */ + public function testOffsetUnsetRemovesSlotAssignment(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + + $this->assertTrue(isset($slotmap[100])); + unset($slotmap[100]); + $this->assertFalse(isset($slotmap[100])); + } + + /** + * @group disconnected + */ + public function testOffsetUnsetDoesNotDoAnythingOnUnassignedSlot(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + + $this->assertFalse(isset($slotmap[5461])); + unset($slotmap[5461]); + $this->assertFalse(isset($slotmap[5461])); + } + + /** + * @group disconnected + */ + public function testOffsetSetAssignsNodeToSlot(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + + $this->assertSame('127.0.0.1:6380', $slotmap[100] = '127.0.0.1:6380'); + $this->assertSame('127.0.0.1:6380', $slotmap[100]); + + $this->assertNull($slotmap[5461]); + $this->assertSame('127.0.0.1:6380', $slotmap[5461] = '127.0.0.1:6380'); + $this->assertSame('127.0.0.1:6380', $slotmap[5461]); + } + + /** + * @group disconnected + */ + public function testOffsetSetCastsValueToString(): void + { + $slotmap = new SlotMap(); + + $connection = $this->getMockConnection(); + $connection + ->expects($this->once()) + ->method('__toString') + ->willReturn('127.0.0.1:6379'); + + $this->assertSame($connection, $slotmap[0] = $connection); + $this->assertSame('127.0.0.1:6379', $slotmap[0]); + } + + /** + * @group disconnected + */ + public function testOffsetSetThrowsExceptionOnInvalidSlot(): void + { + $this->expectException('OutOfBoundsException'); + $this->expectExceptionMessage('Invalid slot 16384 for `127.0.0.1:6379`'); + + $slotmap = new SlotMap(); + + $slotmap[16384] = '127.0.0.1:6379'; + } + + /** + * @group disconnected + */ + public function testGetIteratorReturnsIteratorOverSlotMap(): void + { + $slotmap = new SlotMap(); + + $slotmap->setSlots(0, 5460, '127.0.0.1:6379'); + $slotmap->setSlots(5461, 10922, '127.0.0.1:6380'); + $slotmap->setSlots(10923, 16383, '127.0.0.1:6381'); + + $expectedMap = array_merge( + array_fill(0, 5461, '127.0.0.1:6379'), + array_fill(5461, 5462, '127.0.0.1:6380'), + array_fill(10923, 5461, '127.0.0.1:6381') + ); + + $this->assertSame($expectedMap, iterator_to_array($slotmap)); + } +} diff --git a/tests/Predis/Cluster/SlotRangeTest.php b/tests/Predis/Cluster/SlotRangeTest.php new file mode 100644 index 00000000..5ee66ef7 --- /dev/null +++ b/tests/Predis/Cluster/SlotRangeTest.php @@ -0,0 +1,134 @@ +assertEquals(SlotRange::MAX_SLOTS, 16383); + } + + /** + * @group disconnected + */ + public function testConstructorThrowExceptionOnInvalidSlotRange(): void + { + $this->expectException('OutOfBoundsException'); + $this->expectExceptionMessage('Invalid slot range 600-300 for `c1`'); + + new SlotRange(600, 300, 'c1'); + } + + /** + * @group disconnected + */ + public function testIsValidRangeReturnsTrueOnValidSlotRange() + { + $this->assertTrue(SlotRange::isValidRange(0, SlotRange::MAX_SLOTS)); + $this->assertTrue(SlotRange::isValidRange(2000, 2999)); + $this->assertTrue(SlotRange::isValidRange(3000, 3000)); + } + + /** + * @group disconnected + */ + public function testIsValidRangeReturnsFalseOnInvalidSlotRange() + { + $this->assertFalse(SlotRange::isValidRange(0, 16384)); + $this->assertFalse(SlotRange::isValidRange(-1, 16383)); + $this->assertFalse(SlotRange::isValidRange(-1, 16384)); + $this->assertFalse(SlotRange::isValidRange(2999, 2000)); + } + + /** + * @group disconnected + */ + public function testPropertySetters() + { + $range = new SlotRange(2000, 5000, 'c1'); + + $this->assertEquals(2000, $range->getStart()); + $this->assertEquals(5000, $range->getEnd()); + $this->assertEquals('c1', $range->getConnection()); + } + + /** + * @group disconnected + */ + public function testHasSlot() + { + $range = new SlotRange(2000, 4000, 'c1'); + + $this->assertTrue($range->hasSlot(2000)); + $this->assertTrue($range->hasSlot(4000)); + $this->assertTrue($range->hasSlot(3000)); + + $this->assertFalse($range->hasSlot(1000)); + $this->assertFalse($range->hasSlot(5000)); + } + + /** + * @group disconnected + */ + public function testToArray() + { + $range = new SlotRange(2000, 2005, 'c1'); + $array = $range->toArray(); + $this->assertSame([ + 2000 => 'c1', + 2001 => 'c1', + 2002 => 'c1', + 2003 => 'c1', + 2004 => 'c1', + 2005 => 'c1', + ], $array); + } + + /** + * @group disconnected + */ + public function testCount() + { + $range = new SlotRange(2000, 3000, 'c1'); + $this->assertEquals(1001, $range->count()); + $this->assertEquals(1001, count($range)); + } + + /** + * @group disconnected + */ + public function testHasIntersectionWith() + { + $original = new SlotRange(2000, 3000, 'c1'); + + $this->assertFalse($original->hasIntersectionWith(new SlotRange(1000, 1500, 'c1'))); + $this->assertFalse($original->hasIntersectionWith(new SlotRange(3001, 5000, 'c1'))); + $this->assertFalse($original->hasIntersectionWith(new SlotRange(1999, 1999, 'c1'))); + $this->assertFalse($original->hasIntersectionWith(new SlotRange(3001, 3001, 'c1'))); + + $this->assertTrue($original->hasIntersectionWith(new SlotRange(1500, 6000, 'c1'))); + $this->assertTrue($original->hasIntersectionWith(new SlotRange(2500, 2999, 'c1'))); + $this->assertTrue($original->hasIntersectionWith(new SlotRange(2000, 2999, 'c1'))); + $this->assertTrue($original->hasIntersectionWith(new SlotRange(2500, 3000, 'c1'))); + $this->assertTrue($original->hasIntersectionWith(new SlotRange(1500, 2000, 'c1'))); + $this->assertTrue($original->hasIntersectionWith(new SlotRange(3000, 3500, 'c1'))); + $this->assertTrue($original->hasIntersectionWith(new SlotRange(2000, 2000, 'c1'))); + $this->assertTrue($original->hasIntersectionWith(new SlotRange(3000, 3000, 'c1'))); + } +}