mirror of
https://github.com/predis/predis.git
synced 2026-08-31 04:35:05 +00:00
Added support for HOTKEYS container command (#1630)
* Added testing with SSL authentication using CN * Update CHANGELOG.md * Codestyle changes * Fixed broken tests * Fixed hybdrid tests * Fixed SSL tests * Fixed array merging * Updated README.md * Updated spelling list * Added support for HOTKEYS container command * Added ignoring for PHPUnit package * Update CHANGELOG.md, codestyle fixes, mark tests as relay-incompatible * Added RESP3 test, fixed response parsing * Removed redundant tests * Added cluster test * Fix assertion
This commit is contained in:
committed by
GitHub
parent
53504469d7
commit
861bde010a
@@ -41,7 +41,7 @@ jobs:
|
||||
run: |
|
||||
# Mapping of original redis versions to client test containers
|
||||
declare -A redis_clients_version_mapping=(
|
||||
["8.6"]="8.6-rc1-21356658603-debian-amd64"
|
||||
["8.6"]="custom-21651605017-debian-amd64"
|
||||
["8.4"]="8.4.0"
|
||||
["8.2"]="8.2.2-pre"
|
||||
["8.0"]="8.0.2"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
### Added
|
||||
- Added retry support (#1616)
|
||||
- Added support for VRANGE command (#1623)
|
||||
- Added support for HOTKEYS container command (#1630)
|
||||
|
||||
### Maintenance
|
||||
- Added testing with SSL connection (#1624)
|
||||
|
||||
@@ -42,6 +42,7 @@ use Predis\Command\CommandInterface;
|
||||
use Predis\Command\Container\ACL;
|
||||
use Predis\Command\Container\CLIENT;
|
||||
use Predis\Command\Container\FUNCTIONS;
|
||||
use Predis\Command\Container\HOTKEYS;
|
||||
use Predis\Command\Container\Json\JSONDEBUG;
|
||||
use Predis\Command\Container\Search\FTCONFIG;
|
||||
use Predis\Command\Container\Search\FTCURSOR;
|
||||
@@ -395,6 +396,7 @@ use Predis\Command\Redis\VADD;
|
||||
*
|
||||
* Container commands
|
||||
* @property CLIENT $client
|
||||
* @property HOTKEYS $hotkeys
|
||||
* @property FUNCTIONS $function
|
||||
* @property FTCONFIG $ftconfig
|
||||
* @property FTCURSOR $ftcursor
|
||||
|
||||
@@ -42,6 +42,7 @@ use Predis\Command\CommandInterface;
|
||||
use Predis\Command\Container\ACL;
|
||||
use Predis\Command\Container\CLIENT;
|
||||
use Predis\Command\Container\FUNCTIONS;
|
||||
use Predis\Command\Container\HOTKEYS;
|
||||
use Predis\Command\Container\Json\JSONDEBUG;
|
||||
use Predis\Command\Container\Search\FTCONFIG;
|
||||
use Predis\Command\Container\Search\FTCURSOR;
|
||||
@@ -408,6 +409,7 @@ use Predis\Response\Status;
|
||||
*
|
||||
* Container commands
|
||||
* @property CLIENT $client
|
||||
* @property HOTKEYS $hotkeys
|
||||
* @property FUNCTIONS $function
|
||||
* @property FTCONFIG $ftconfig
|
||||
* @property FTCURSOR $ftcursor
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2025 Till Krüss
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Command\Container;
|
||||
|
||||
use Predis\Response\Status;
|
||||
|
||||
/**
|
||||
* @method Status stop()
|
||||
* @method Status reset()
|
||||
* @method array get()
|
||||
*/
|
||||
class HOTKEYS extends AbstractContainer
|
||||
{
|
||||
public const CPU = 'CPU';
|
||||
public const NET = 'NET';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContainerCommandId(): string
|
||||
{
|
||||
return 'HOTKEYS';
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a hotkeys tracking on server side.
|
||||
*
|
||||
* @param array<self::CPU|self::NET> $metrics One of the available metric types. Check class constants.
|
||||
* @param int|null $count Number of top keys to report. Default: 10, Min: 10, Max: 64
|
||||
* @param int|null $duration Auto-stop tracking after this many seconds. Default: 0 (no auto-stop)
|
||||
* @param int|null $sample Sample ratio - track keys with probability 1/sample. Default: 1 (track every key), Min: 1
|
||||
* @param array<int>|null $slots All specified slots must be hosted by the receiving node! If not specified, all slots are tracked.
|
||||
* @return string|Status
|
||||
*/
|
||||
public function start(array $metrics, ?int $count = null, ?int $duration = null, ?int $sample = null, ?array $slots = null)
|
||||
{
|
||||
return $this->__call('START', func_get_args());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2025 Till Krüss
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Command\Redis;
|
||||
|
||||
use Predis\Command\Command as RedisCommand;
|
||||
use Predis\Command\Redis\Utils\CommandUtility;
|
||||
use ValueError;
|
||||
|
||||
class HOTKEYS extends RedisCommand
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return 'HOTKEYS';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $arguments
|
||||
* @return void
|
||||
*/
|
||||
public function setArguments(array $arguments)
|
||||
{
|
||||
switch ($arguments[0]) {
|
||||
case 'START':
|
||||
$this->setStartArguments($arguments);
|
||||
break;
|
||||
|
||||
default:
|
||||
parent::setArguments($arguments);
|
||||
}
|
||||
}
|
||||
|
||||
public function parseResponse($data)
|
||||
{
|
||||
if (is_array($data)) {
|
||||
foreach ($data as $key => $item) {
|
||||
$dict = CommandUtility::arrayToDictionary($item, null, false);
|
||||
$data[$key] = $dict;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $arguments
|
||||
* @return void
|
||||
*/
|
||||
private function setStartArguments(array $arguments)
|
||||
{
|
||||
$processedArguments = [$arguments[0]];
|
||||
|
||||
array_push($processedArguments, 'METRICS', count($arguments[1]), ...$arguments[1]);
|
||||
|
||||
if (isset($arguments[2])) {
|
||||
if ($arguments[2] > 9 && $arguments[2] < 65) {
|
||||
array_push($processedArguments, 'COUNT', $arguments[2]);
|
||||
} else {
|
||||
throw new ValueError('Count value should be between 10 and 64');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($arguments[3])) {
|
||||
array_push($processedArguments, 'DURATION', $arguments[3]);
|
||||
}
|
||||
|
||||
if (isset($arguments[4])) {
|
||||
if ($arguments[4] > 0) {
|
||||
array_push($processedArguments, 'SAMPLE', $arguments[4]);
|
||||
} else {
|
||||
throw new ValueError('Sample value should be greater than 0');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($arguments[5])) {
|
||||
array_push($processedArguments, 'SLOTS', count($arguments[5]), ...$arguments[5]);
|
||||
}
|
||||
|
||||
parent::setArguments($processedArguments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2025 Till Krüss
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Predis\Command\Redis;
|
||||
|
||||
use Predis\Command\Container\HOTKEYS as Container;
|
||||
use Predis\NotSupportedException;
|
||||
use ValueError;
|
||||
|
||||
/**
|
||||
* @group commands
|
||||
* @group relay-incompatible
|
||||
* @group realm-generic
|
||||
*/
|
||||
class HOTKEYS_Test extends PredisCommandTestCase
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCommand(): string
|
||||
{
|
||||
return HOTKEYS::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedId(): string
|
||||
{
|
||||
return 'HOTKEYS';
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @dataProvider argumentsProvider
|
||||
*/
|
||||
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
$command->setArguments($actualArguments);
|
||||
|
||||
$this->assertSame($expectedArguments, $command->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testParseResponse(): void
|
||||
{
|
||||
$this->assertSame(1, $this->getCommand()->parseResponse(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.5.0
|
||||
* @return void
|
||||
*/
|
||||
public function testRetrieveHotKeys()
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
// Starts hotkeys tracking (CPU only)
|
||||
$this->assertEquals('OK', $redis->hotkeys->start([Container::CPU]));
|
||||
$this->assertEquals('OK', $redis->set('key', 'value'));
|
||||
$this->assertEquals('OK', $redis->hotkeys->stop());
|
||||
|
||||
$hotkeysInfo = $redis->hotkeys->get()[0];
|
||||
$this->assertContains('key', $hotkeysInfo['by-cpu-time-us']);
|
||||
|
||||
// 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'));
|
||||
$this->assertEquals('OK', $redis->hotkeys->stop());
|
||||
|
||||
$hotkeysInfo = $redis->hotkeys->get()[0];
|
||||
|
||||
$this->assertContains('key', $hotkeysInfo['by-cpu-time-us']);
|
||||
$this->assertContains('key1', $hotkeysInfo['by-cpu-time-us']);
|
||||
$this->assertContains('key', $hotkeysInfo['by-net-bytes']);
|
||||
$this->assertContains('key1', $hotkeysInfo['by-net-bytes']);
|
||||
|
||||
// Starts hotkeys tracking (limited COUNT)
|
||||
$this->assertEquals('OK', $redis->hotkeys->start([Container::CPU, Container::NET], 12));
|
||||
|
||||
for ($i = 0; $i < 13; $i++) {
|
||||
$redis->set("key:$i", "value:$i");
|
||||
}
|
||||
$this->assertEquals('OK', $redis->hotkeys->stop());
|
||||
|
||||
$hotkeysInfo = $redis->hotkeys->get()[0];
|
||||
$this->assertCount(24, $hotkeysInfo['by-cpu-time-us']);
|
||||
|
||||
// Starts hotkeys tracking (with DURATION, SAMPLE)
|
||||
$this->assertEquals(
|
||||
'OK',
|
||||
$redis->hotkeys->start([Container::CPU, Container::NET], null, 1, 10)
|
||||
);
|
||||
$this->sleep(1.2);
|
||||
|
||||
$hotkeysInfo = $redis->hotkeys->get()[0];
|
||||
$this->assertEquals(0, $hotkeysInfo['tracking-active']);
|
||||
$this->assertEquals(10, $hotkeysInfo['sample-ratio']);
|
||||
|
||||
$this->assertEquals('OK', $redis->hotkeys->reset());
|
||||
|
||||
$hotkeysInfo = $redis->hotkeys->get()[0];
|
||||
$this->assertEquals(0, $hotkeysInfo['tracking-active']);
|
||||
$this->assertNull($hotkeysInfo['sample-ratio']);
|
||||
$this->assertEmpty($hotkeysInfo['selected-slots']);
|
||||
$this->assertEmpty($hotkeysInfo['by-cpu-time-us']);
|
||||
$this->assertEmpty($hotkeysInfo['by-net-bytes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.5.0
|
||||
* @return void
|
||||
*/
|
||||
public function testRetrieveHotKeysResp3()
|
||||
{
|
||||
$redis = $this->getResp3Client();
|
||||
|
||||
// Starts hotkeys tracking (CPU only)
|
||||
$this->assertEquals('OK', $redis->hotkeys->start([Container::CPU]));
|
||||
$this->assertEquals('OK', $redis->set('key', 'value'));
|
||||
$this->assertEquals('OK', $redis->hotkeys->stop());
|
||||
|
||||
$hotkeysInfo = $redis->hotkeys->get()[0];
|
||||
$this->assertContains('key', $hotkeysInfo['by-cpu-time-us']);
|
||||
|
||||
// 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'));
|
||||
$this->assertEquals('OK', $redis->hotkeys->stop());
|
||||
|
||||
$hotkeysInfo = $redis->hotkeys->get()[0];
|
||||
|
||||
$this->assertContains('key', $hotkeysInfo['by-cpu-time-us']);
|
||||
$this->assertContains('key1', $hotkeysInfo['by-cpu-time-us']);
|
||||
$this->assertContains('key', $hotkeysInfo['by-net-bytes']);
|
||||
$this->assertContains('key1', $hotkeysInfo['by-net-bytes']);
|
||||
|
||||
// Starts hotkeys tracking (limited COUNT)
|
||||
$this->assertEquals('OK', $redis->hotkeys->start([Container::CPU, Container::NET], 12));
|
||||
|
||||
for ($i = 0; $i < 13; $i++) {
|
||||
$redis->set("key:$i", "value:$i");
|
||||
}
|
||||
$this->assertEquals('OK', $redis->hotkeys->stop());
|
||||
|
||||
$hotkeysInfo = $redis->hotkeys->get()[0];
|
||||
$this->assertCount(24, $hotkeysInfo['by-cpu-time-us']);
|
||||
|
||||
// Starts hotkeys tracking (with DURATION, SAMPLE)
|
||||
$this->assertEquals(
|
||||
'OK',
|
||||
$redis->hotkeys->start([Container::CPU, Container::NET], null, 1, 10)
|
||||
);
|
||||
$this->sleep(1.2);
|
||||
|
||||
$hotkeysInfo = $redis->hotkeys->get()[0];
|
||||
$this->assertEquals(0, $hotkeysInfo['tracking-active']);
|
||||
$this->assertEquals(10, $hotkeysInfo['sample-ratio']);
|
||||
|
||||
$this->assertEquals('OK', $redis->hotkeys->reset());
|
||||
|
||||
$hotkeysInfo = $redis->hotkeys->get()[0];
|
||||
$this->assertEquals(0, $hotkeysInfo['tracking-active']);
|
||||
$this->assertNull($hotkeysInfo['sample-ratio']);
|
||||
$this->assertEmpty($hotkeysInfo['selected-slots']);
|
||||
$this->assertEmpty($hotkeysInfo['by-cpu-time-us']);
|
||||
$this->assertEmpty($hotkeysInfo['by-net-bytes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @group cluster
|
||||
* @requiresRedisVersion >= 8.5.0
|
||||
* @return void
|
||||
*/
|
||||
public function testHotkeysStartDisabledInClusterClient()
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
$this->expectException(NotSupportedException::class);
|
||||
$this->expectExceptionMessage("Cannot use 'HOTKEYS' with redis-cluster");
|
||||
|
||||
$redis->hotkeys->start([Container::CPU]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testThrowsExceptionOnInvalidSampleValue(): void
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
|
||||
$this->expectException(ValueError::class);
|
||||
$this->expectExceptionMessage('Sample value should be greater than 0');
|
||||
|
||||
$command->setArguments(['START', ['metric1', 'metric2'], null, null, 0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testThrowsExceptionOnNegativeSampleValue(): void
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
|
||||
$this->expectException(ValueError::class);
|
||||
$this->expectExceptionMessage('Sample value should be greater than 0');
|
||||
|
||||
$command->setArguments(['START', ['metric1', 'metric2'], null, null, -1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testThrowsExceptionOnCountValueTooLow(): void
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
|
||||
$this->expectException(ValueError::class);
|
||||
$this->expectExceptionMessage('Count value should be between 10 and 64');
|
||||
|
||||
$command->setArguments(['START', ['metric1', 'metric2'], 9]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testThrowsExceptionOnCountValueTooHigh(): void
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
|
||||
$this->expectException(ValueError::class);
|
||||
$this->expectExceptionMessage('Count value should be between 10 and 64');
|
||||
|
||||
$command->setArguments(['START', ['metric1', 'metric2'], 65]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testThrowsExceptionOnCountValueZero(): void
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
|
||||
$this->expectException(ValueError::class);
|
||||
$this->expectExceptionMessage('Count value should be between 10 and 64');
|
||||
|
||||
$command->setArguments(['START', ['metric1', 'metric2'], 0]);
|
||||
}
|
||||
|
||||
public function argumentsProvider(): array
|
||||
{
|
||||
return [
|
||||
'with non-START subcommand' => [
|
||||
['STOP'],
|
||||
['STOP'],
|
||||
],
|
||||
'with START and metrics only' => [
|
||||
['START', ['metric1', 'metric2']],
|
||||
['START', 'METRICS', 2, 'metric1', 'metric2'],
|
||||
],
|
||||
'with START, metrics and COUNT' => [
|
||||
['START', ['metric1', 'metric2'], 50],
|
||||
['START', 'METRICS', 2, 'metric1', 'metric2', 'COUNT', 50],
|
||||
],
|
||||
'with START, metrics, COUNT and DURATION' => [
|
||||
['START', ['metric1', 'metric2'], 50, 60],
|
||||
['START', 'METRICS', 2, 'metric1', 'metric2', 'COUNT', 50, 'DURATION', 60],
|
||||
],
|
||||
'with START, metrics, COUNT, DURATION and SAMPLE' => [
|
||||
['START', ['metric1', 'metric2'], 50, 60, 10],
|
||||
['START', 'METRICS', 2, 'metric1', 'metric2', 'COUNT', 50, 'DURATION', 60, 'SAMPLE', 10],
|
||||
],
|
||||
'with START, metrics, COUNT, DURATION, SAMPLE and SLOTS' => [
|
||||
['START', ['metric1', 'metric2'], 50, 60, 10, [1, 2, 3]],
|
||||
['START', 'METRICS', 2, 'metric1', 'metric2', 'COUNT', 50, 'DURATION', 60, 'SAMPLE', 10, 'SLOTS', 3, 1, 2, 3],
|
||||
],
|
||||
'with START, metrics and SLOTS (no COUNT, DURATION, SAMPLE)' => [
|
||||
['START', ['metric1'], null, null, null, [5, 10]],
|
||||
['START', 'METRICS', 1, 'metric1', 'SLOTS', 2, 5, 10],
|
||||
],
|
||||
'with START, metrics, COUNT and SLOTS (no DURATION, SAMPLE)' => [
|
||||
['START', ['metric1'], 50, null, null, [1, 2]],
|
||||
['START', 'METRICS', 1, 'metric1', 'COUNT', 50, 'SLOTS', 2, 1, 2],
|
||||
],
|
||||
'with START, metrics, COUNT, DURATION and SLOTS (no SAMPLE)' => [
|
||||
['START', ['metric1'], 50, 30, null, [1]],
|
||||
['START', 'METRICS', 1, 'metric1', 'COUNT', 50, 'DURATION', 30, 'SLOTS', 1, 1],
|
||||
],
|
||||
'with START and single metric' => [
|
||||
['START', ['metric1']],
|
||||
['START', 'METRICS', 1, 'metric1'],
|
||||
],
|
||||
'with START and multiple metrics' => [
|
||||
['START', ['metric1', 'metric2', 'metric3']],
|
||||
['START', 'METRICS', 3, 'metric1', 'metric2', 'metric3'],
|
||||
],
|
||||
'with START, metrics and minimum valid COUNT (10)' => [
|
||||
['START', ['metric1'], 10],
|
||||
['START', 'METRICS', 1, 'metric1', 'COUNT', 10],
|
||||
],
|
||||
'with START, metrics and maximum valid COUNT (64)' => [
|
||||
['START', ['metric1'], 64],
|
||||
['START', 'METRICS', 1, 'metric1', 'COUNT', 64],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user