diff --git a/.github/docker-compose.yml b/.github/docker-compose.yml index cd574ab2..1ea14c2d 100644 --- a/.github/docker-compose.yml +++ b/.github/docker-compose.yml @@ -71,7 +71,7 @@ services: - TLS_CLIENT_CNS=test_user - PORT=6372 - TLS_PORT=27379 - command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save "" --requirepass "foobar"} + command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save "" --requirepass "foobar"} ${REDIS_CLUSTER_EXTRA_ARGS:-} ports: - "6372-6377:6372-6377" - "27379-27384:27379-27384" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6021db52..0d46930d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,10 +35,7 @@ jobs: - '7.2' - '7.4' - '8.2' - - '8.4' - - '8.6' - - '8.8' - - '8.10' + - '8.12' steps: @@ -46,6 +43,7 @@ jobs: run: | # Mapping of original redis versions to client test containers declare -A redis_clients_version_mapping=( + ["8.12"]="unstable-35791556614-debian" ["8.10"]="8.10.0" ["8.8"]="8.8.0" ["8.6"]="8.6.1" @@ -67,6 +65,7 @@ jobs: echo "DOCKER_SERVICE=redis-clients" >> $GITHUB_ENV redis_major_version=$(echo "${{ matrix.redis }}" | grep -oP '^\d+') + redis_minor_version=$(echo "${{ matrix.redis }}" | grep -oP '(?<=\.)\d+') # Some configuration options available since Redis > 7 if (( redis_major_version < 7 )); then @@ -76,6 +75,13 @@ jobs: echo "REDIS_STACK_SERVER_PORT=6379" >> $GITHUB_ENV fi + # Since 8.12, cluster-bus-port-protected-mode defaults to yes, which + # aborts cluster startup unless explicitly disabled or tls-cluster + # is enabled. + if (( redis_major_version > 8 || (redis_major_version == 8 && redis_minor_version >= 12) )); then + echo "REDIS_CLUSTER_EXTRA_ARGS=--cluster-bus-port-protected-mode no" >> $GITHUB_ENV + fi + else echo "REDIS_IMAGE_NAME=redis:${{ matrix.redis }}" >> $GITHUB_ENV echo "DOCKER_SERVICE=redis-official" >> $GITHUB_ENV diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e1d0f1..37d2fa7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added optional `CELL_SIZE` argument for `CMS.INITBYDIM` and `CMS.INITBYPROB` ### Changed ### Fixed diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 704beb15..9829e335 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -132,8 +132,8 @@ use Predis\Command\Redis\VADD; * @method $this cfscandump(string $key, int $iterator) * @method $this cmsincrby(string $key, string|int ...$itemIncrementDictionary) * @method $this cmsinfo(string $key) - * @method $this cmsinitbydim(string $key, int $width, int $depth) - * @method $this cmsinitbyprob(string $key, float $errorRate, float $probability) + * @method $this cmsinitbydim(string $key, int $width, int $depth, ?int $cellSize = null) + * @method $this cmsinitbyprob(string $key, float $errorRate, float $probability, ?int $cellSize = null) * @method $this cmsmerge(string $destination, array $sources, array $weights = []) * @method $this cmsquery(string $key, string ...$item) * @method $this decr($key) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index fc61dcc0..ccdce375 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -143,8 +143,8 @@ use Predis\Response\Status; * @method array cfscandump(string $key, int $iterator) * @method array cmsincrby(string $key, string|int ...$itemIncrementDictionary) * @method array cmsinfo(string $key) - * @method Status cmsinitbydim(string $key, int $width, int $depth) - * @method Status cmsinitbyprob(string $key, float $errorRate, float $probability) + * @method Status cmsinitbydim(string $key, int $width, int $depth, ?int $cellSize = null) + * @method Status cmsinitbyprob(string $key, float $errorRate, float $probability, ?int $cellSize = null) * @method Status cmsmerge(string $destination, array $sources, array $weights = []) * @method array cmsquery(string $key, string ...$item) * @method int decr(string $key) diff --git a/src/Command/Redis/CountMinSketch/CMSINITBYDIM.php b/src/Command/Redis/CountMinSketch/CMSINITBYDIM.php index 5ac34e2f..bcc97dc8 100644 --- a/src/Command/Redis/CountMinSketch/CMSINITBYDIM.php +++ b/src/Command/Redis/CountMinSketch/CMSINITBYDIM.php @@ -26,6 +26,26 @@ class CMSINITBYDIM extends RedisCommand return 'CMS.INITBYDIM'; } + /** + * {@inheritdoc} + * + * Arguments: [key, width, depth, ?cellSize] + */ + public function setArguments(array $arguments) + { + if (array_key_exists(3, $arguments)) { + $cellSize = $arguments[3]; + $arguments = array_slice($arguments, 0, 3); + + if ($cellSize !== null) { + $arguments[] = 'CELL_SIZE'; + $arguments[] = $cellSize; + } + } + + parent::setArguments($arguments); + } + public function prefixKeys($prefix) { $this->applyPrefixForFirstArgument($prefix); diff --git a/src/Command/Redis/CountMinSketch/CMSINITBYPROB.php b/src/Command/Redis/CountMinSketch/CMSINITBYPROB.php index 8e8f4600..7f40aa0f 100644 --- a/src/Command/Redis/CountMinSketch/CMSINITBYPROB.php +++ b/src/Command/Redis/CountMinSketch/CMSINITBYPROB.php @@ -26,6 +26,26 @@ class CMSINITBYPROB extends RedisCommand return 'CMS.INITBYPROB'; } + /** + * {@inheritdoc} + * + * Arguments: [key, errorRate, probability, ?cellSize] + */ + public function setArguments(array $arguments) + { + if (array_key_exists(3, $arguments)) { + $cellSize = $arguments[3]; + $arguments = array_slice($arguments, 0, 3); + + if ($cellSize !== null) { + $arguments[] = 'CELL_SIZE'; + $arguments[] = $cellSize; + } + } + + parent::setArguments($arguments); + } + public function prefixKeys($prefix) { $this->applyPrefixForFirstArgument($prefix); diff --git a/tests/Predis/Command/Redis/CountMinSketch/CMSINFO_Test.php b/tests/Predis/Command/Redis/CountMinSketch/CMSINFO_Test.php index b9e68e4d..c50c8ce3 100644 --- a/tests/Predis/Command/Redis/CountMinSketch/CMSINFO_Test.php +++ b/tests/Predis/Command/Redis/CountMinSketch/CMSINFO_Test.php @@ -95,7 +95,9 @@ class CMSINFO_Test extends PredisCommandTestCase $actualResponse = $redis->cmsinfo('key'); - $this->assertSame($expectedResponse, $actualResponse); + foreach ($expectedResponse as $value) { + $this->assertContains($value, $actualResponse); + } } /** @@ -113,7 +115,9 @@ class CMSINFO_Test extends PredisCommandTestCase $actualResponse = $redis->cmsinfo('key'); - $this->assertSame($expectedResponse, $actualResponse); + foreach ($expectedResponse as $value) { + $this->assertContains($value, $actualResponse); + } } /** diff --git a/tests/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM_Test.php b/tests/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM_Test.php index 02f9a7bb..8b583482 100644 --- a/tests/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM_Test.php +++ b/tests/Predis/Command/Redis/CountMinSketch/CMSINITBYDIM_Test.php @@ -52,6 +52,34 @@ class CMSINITBYDIM_Test extends PredisCommandTestCase $this->assertSameValues($expectedArguments, $command->getArguments()); } + /** + * @group disconnected + */ + public function testFilterArgumentsWithCellSize(): void + { + $actualArguments = ['key', 2000, 10, 2]; + $expectedArguments = ['key', 2000, 10, 'CELL_SIZE', 2]; + + $command = $this->getCommand(); + $command->setArguments($actualArguments); + + $this->assertSameValues($expectedArguments, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testFilterArgumentsWithNullCellSizeOmitsIt(): void + { + $actualArguments = ['key', 2000, 10, null]; + $expectedArguments = ['key', 2000, 10]; + + $command = $this->getCommand(); + $command->setArguments($actualArguments); + + $this->assertSameValues($expectedArguments, $command->getArguments()); + } + /** * @group disconnected */ @@ -113,6 +141,40 @@ class CMSINITBYDIM_Test extends PredisCommandTestCase $this->assertSame(10, $info['depth']); } + /** + * @group connected + * @group relay-resp3 + * @return void + * @requiresRedisVersion >= 8.12.0 + */ + public function testInitializeCountMinSketchWithGivenCellSize(): void + { + $redis = $this->getClient(); + + $actualResponse = $redis->cmsinitbydim('key', 2000, 10, 2); + $info = $redis->cmsinfo('key'); + + $this->assertEquals('OK', $actualResponse); + $this->assertSame(2000, $info['width']); + $this->assertSame(10, $info['depth']); + $this->assertSame(2, $info['cell_size']); + } + + /** + * @group connected + * @group relay-resp3 + * @requiresRedisVersion >= 8.12.0 + */ + public function testThrowsExceptionOnInvalidCellSize(): void + { + $this->expectException(ServerException::class); + $this->expectExceptionMessage('CMS: CELL_SIZE must be 1, 2, 4 or 8'); + + $redis = $this->getClient(); + + $redis->cmsinitbydim('cmsinitbydim_invalid_cellsize', 2000, 10, 3); + } + /** * @group connected * @group relay-resp3 diff --git a/tests/Predis/Command/Redis/CountMinSketch/CMSINITBYPROB_Test.php b/tests/Predis/Command/Redis/CountMinSketch/CMSINITBYPROB_Test.php index 2ba68ad3..c95743bc 100644 --- a/tests/Predis/Command/Redis/CountMinSketch/CMSINITBYPROB_Test.php +++ b/tests/Predis/Command/Redis/CountMinSketch/CMSINITBYPROB_Test.php @@ -52,6 +52,34 @@ class CMSINITBYPROB_Test extends PredisCommandTestCase $this->assertSameValues($expectedArguments, $command->getArguments()); } + /** + * @group disconnected + */ + public function testFilterArgumentsWithCellSize(): void + { + $actualArguments = ['key', 0.001, 0.01, 8]; + $expectedArguments = ['key', 0.001, 0.01, 'CELL_SIZE', 8]; + + $command = $this->getCommand(); + $command->setArguments($actualArguments); + + $this->assertSameValues($expectedArguments, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testFilterArgumentsWithNullCellSizeOmitsIt(): void + { + $actualArguments = ['key', 0.001, 0.01, null]; + $expectedArguments = ['key', 0.001, 0.01]; + + $command = $this->getCommand(); + $command->setArguments($actualArguments); + + $this->assertSameValues($expectedArguments, $command->getArguments()); + } + /** * @group disconnected */ @@ -113,6 +141,40 @@ class CMSINITBYPROB_Test extends PredisCommandTestCase $this->assertSame(7, $info['depth']); } + /** + * @group connected + * @group relay-resp3 + * @return void + * @requiresRedisVersion >= 8.12.0 + */ + public function testInitializeCountMinSketchWithGivenCellSize(): void + { + $redis = $this->getClient(); + + $actualResponse = $redis->cmsinitbyprob('key', 0.001, 0.01, 8); + $info = $redis->cmsinfo('key'); + + $this->assertEquals('OK', $actualResponse); + $this->assertSame(2000, $info['width']); + $this->assertSame(7, $info['depth']); + $this->assertSame(8, $info['cell_size']); + } + + /** + * @group connected + * @group relay-resp3 + * @requiresRedisVersion >= 8.12.0 + */ + public function testThrowsExceptionOnInvalidCellSize(): void + { + $this->expectException(ServerException::class); + $this->expectExceptionMessage('CMS: CELL_SIZE must be 1, 2, 4 or 8'); + + $redis = $this->getClient(); + + $redis->cmsinitbyprob('cmsinitbyprob_invalid_cellsize', 0.001, 0.01, 3); + } + /** * @group connected * @group relay-resp3 diff --git a/tests/Predis/Command/Redis/HOTKEYS_Test.php b/tests/Predis/Command/Redis/HOTKEYS_Test.php index ae49a1d6..9295aee0 100644 --- a/tests/Predis/Command/Redis/HOTKEYS_Test.php +++ b/tests/Predis/Command/Redis/HOTKEYS_Test.php @@ -70,7 +70,12 @@ class HOTKEYS_Test extends PredisCommandTestCase // Starts hotkeys tracking (CPU only) $this->assertEquals('OK', $redis->hotkeys->start([Container::CPU])); - $this->assertEquals('OK', $redis->set('key', 'value')); + + // A single SET's CPU time can round down to 0 and be excluded from tracking. + for ($i = 0; $i < 5; $i++) { + $redis->set('key', 'value'); + } + $this->assertEquals('OK', $redis->hotkeys->stop()); $hotkeysInfo = $redis->hotkeys->get()[0]; @@ -78,8 +83,12 @@ class HOTKEYS_Test extends PredisCommandTestCase // Starts hotkeys tracking (CPU and NET) $this->assertEquals('OK', $redis->hotkeys->start([Container::CPU, Container::NET])); - $this->assertEquals('OK', $redis->set('key', 'value')); - $this->assertEquals('OK', $redis->set('key1', 'value1')); + + for ($i = 0; $i < 5; $i++) { + $redis->set('key', 'value'); + $redis->set('key1', 'value1'); + } + $this->assertEquals('OK', $redis->hotkeys->stop()); $hotkeysInfo = $redis->hotkeys->get()[0]; @@ -137,7 +146,12 @@ class HOTKEYS_Test extends PredisCommandTestCase // Starts hotkeys tracking (CPU only) $this->assertEquals('OK', $redis->hotkeys->start([Container::CPU])); - $this->assertEquals('OK', $redis->set('key', 'value')); + + // A single SET's CPU time can round down to 0 and be excluded from tracking. + for ($i = 0; $i < 5; $i++) { + $redis->set('key', 'value'); + } + $this->assertEquals('OK', $redis->hotkeys->stop()); $hotkeysInfo = $redis->hotkeys->get()[0]; @@ -145,8 +159,12 @@ class HOTKEYS_Test extends PredisCommandTestCase // Starts hotkeys tracking (CPU and NET) $this->assertEquals('OK', $redis->hotkeys->start([Container::CPU, Container::NET])); - $this->assertEquals('OK', $redis->set('key', 'value')); - $this->assertEquals('OK', $redis->set('key1', 'value1')); + + for ($i = 0; $i < 5; $i++) { + $redis->set('key', 'value'); + $redis->set('key1', 'value1'); + } + $this->assertEquals('OK', $redis->hotkeys->stop()); $hotkeysInfo = $redis->hotkeys->get()[0]; diff --git a/tests/Predis/Command/Redis/Search/FTSEARCH_Test.php b/tests/Predis/Command/Redis/Search/FTSEARCH_Test.php index 3a05dbac..459e7d3e 100644 --- a/tests/Predis/Command/Redis/Search/FTSEARCH_Test.php +++ b/tests/Predis/Command/Redis/Search/FTSEARCH_Test.php @@ -23,6 +23,7 @@ use Predis\Command\Argument\Search\SchemaFields\TextField; use Predis\Command\Argument\Search\SchemaFields\VectorField; use Predis\Command\Argument\Search\SearchArguments; use Predis\Command\PrefixableCommand; +use Predis\Command\RawCommand; use Predis\Command\Redis\PredisCommandTestCase; use Predis\Command\Redis\Utils\VectorUtility; use Predis\Response\ServerException; @@ -690,11 +691,17 @@ class FTSEARCH_Test extends PredisCommandTestCase $this->createSearchTimeoutIndex($redis); $this->addDataForSearchTimeout($redis); - $response = $redis->ftsearch( - 'idx', - sprintf('*=>[KNN %d @embedding $vec]', self::SEARCH_TIMEOUT_DOCS), - $this->getSearchTimeoutArguments() - ); + $this->enableVecsimMockTimeout($redis); + + try { + $response = $redis->ftsearch( + 'idx', + sprintf('*=>[KNN %d @embedding $vec]', self::SEARCH_TIMEOUT_DOCS), + $this->getSearchTimeoutArguments() + ); + } finally { + $this->disableVecsimMockTimeout($redis); + } // Only the RESP3 wire carries the server timeout warning. $this->assertIsInt($response['total_results']); @@ -724,6 +731,8 @@ class FTSEARCH_Test extends PredisCommandTestCase $this->expectException(ServerException::class); $this->expectExceptionMessage('Timeout limit was reached'); + $this->enableVecsimMockTimeout($redis); + try { $this->assertEquals('OK', $redis->config('SET', 'search-on-timeout', 'fail')); @@ -734,6 +743,7 @@ class FTSEARCH_Test extends PredisCommandTestCase ); } finally { $redis->config('SET', 'search-on-timeout', $originalPolicy); + $this->disableVecsimMockTimeout($redis); } } @@ -776,6 +786,17 @@ class FTSEARCH_Test extends PredisCommandTestCase } } + // Forces a deterministic timeout instead of racing a real KNN scan against a 1ms budget. + private function enableVecsimMockTimeout(ClientInterface $redis): void + { + $redis->executeCommand(RawCommand::create('_FT.DEBUG', 'VECSIM_MOCK_TIMEOUT', 'enable')); + } + + private function disableVecsimMockTimeout(ClientInterface $redis): void + { + $redis->executeCommand(RawCommand::create('_FT.DEBUG', 'VECSIM_MOCK_TIMEOUT', 'disable')); + } + private function createSearchTimeoutIndex(ClientInterface $redis): void { $schema = [ diff --git a/tests/Predis/Command/Redis/TimeSeries/TSINFO_Test.php b/tests/Predis/Command/Redis/TimeSeries/TSINFO_Test.php index c9400fb4..18cd6c48 100644 --- a/tests/Predis/Command/Redis/TimeSeries/TSINFO_Test.php +++ b/tests/Predis/Command/Redis/TimeSeries/TSINFO_Test.php @@ -87,10 +87,10 @@ class TSINFO_Test extends PredisCommandTestCase public function testReturnsInformationAboutGivenTimeSeries(): void { $redis = $this->getClient(); - $expectedResponse = ['totalSamples', 0, 'memoryUsage', 5000, 'firstTimestamp', 0, 'lastTimestamp', 0, - 'retentionTime', 60000, 'chunkCount', 1, 'chunkSize', 4096, 'chunkType', 'compressed', 'duplicatePolicy', - 'max', 'labels', [['sensor_id', '2'], ['area_id', '32']], 'sourceKey', null, 'rules', [], - 'ignoreMaxTimeDiff', 0, 'ignoreMaxValDiff', 0]; + $expectedResponse = ['totalSamples' => 0, 'memoryUsage' => 5000, 'firstTimestamp' => 0, 'lastTimestamp' => 0, + 'retentionTime' => 60000, 'chunkCount' => 1, 'chunkSize' => 4096, 'chunkType' => 'compressed', + 'duplicatePolicy' => 'max', 'labels' => [['sensor_id', '2'], ['area_id', '32']], 'sourceKey' => null, + 'rules' => [], 'ignoreMaxTimeDiff' => 0, 'ignoreMaxValDiff' => 0]; $arguments = (new CreateArguments()) ->retentionMsecs(60000) @@ -102,7 +102,15 @@ class TSINFO_Test extends PredisCommandTestCase $redis->tscreate('temperature:2:32', $arguments) ); - $this->assertEqualsWithDelta($expectedResponse, $redis->tsinfo('temperature:2:32'), 1000); + $actualResponse = self::pairsToMap($redis->tsinfo('temperature:2:32')); + + // memoryUsage drifts across module releases; ignoreMaxValDiff comes + // back as a numeric string on RESP2. Normalize both before comparing. + $this->assertIsInt($actualResponse['memoryUsage']); + $actualResponse['memoryUsage'] = $expectedResponse['memoryUsage']; + $actualResponse['ignoreMaxValDiff'] = (int) $actualResponse['ignoreMaxValDiff']; + + $this->assertEquals($expectedResponse, $actualResponse); } /** @@ -130,7 +138,26 @@ class TSINFO_Test extends PredisCommandTestCase $redis->tscreate('temperature:2:32', $arguments) ); - $this->assertEqualsWithDelta($expectedResponse, $redis->tsinfo('temperature:2:32'), 1000); + $actualResponse = $redis->tsinfo('temperature:2:32'); + + // ignoreMaxValDiff comes back as a double (0.0) on RESP3. + $this->assertIsInt($actualResponse['memoryUsage']); + $actualResponse['memoryUsage'] = $expectedResponse['memoryUsage']; + $actualResponse['ignoreMaxValDiff'] = (int) $actualResponse['ignoreMaxValDiff']; + + $this->assertEquals($expectedResponse, $actualResponse); + } + + // Converts TS.INFO's flat RESP2 reply into an associative array. + private static function pairsToMap(array $pairs): array + { + $map = []; + + for ($i = 0, $iMax = count($pairs); $i < $iMax; $i += 2) { + $map[$pairs[$i]] = $pairs[$i + 1]; + } + + return $map; } public function argumentsProvider(): array