mirror of
https://github.com/predis/predis.git
synced 2026-09-14 20:37:29 +00:00
Added support for GCRA command (#1657)
* Added support for GCRA command * Codestyle fixes * Updated test image
This commit is contained in:
committed by
GitHub
parent
bb37322a24
commit
7a4fa99d13
@@ -34,6 +34,7 @@ jobs:
|
||||
- '8.2'
|
||||
- '8.4'
|
||||
- '8.6'
|
||||
- '8.8'
|
||||
|
||||
steps:
|
||||
|
||||
@@ -41,7 +42,8 @@ jobs:
|
||||
run: |
|
||||
# Mapping of original redis versions to client test containers
|
||||
declare -A redis_clients_version_mapping=(
|
||||
["8.6"]="8.6.0"
|
||||
["8.8"]="unstable-23321515778-debian"
|
||||
["8.6"]="8.6.1"
|
||||
["8.4"]="8.4.0"
|
||||
["8.2"]="8.2.2"
|
||||
["8.0"]="8.0.2"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
- Added support for GCRA command (#1657)
|
||||
- Handle `-READONLY` responses in Redis Cluster for AWS ElastiCache Redis OSS failover events
|
||||
### Changed
|
||||
- Include command name in unsupported container command error messages (#1653)
|
||||
|
||||
@@ -142,6 +142,7 @@ use Predis\Command\Redis\VADD;
|
||||
* @method $this ftsyndump(string $index)
|
||||
* @method $this ftsynupdate(string $index, string $synonymGroupId, ?SynUpdateArguments $arguments = null, string ...$terms)
|
||||
* @method $this fttagvals(string $index, string $fieldName)
|
||||
* @method $this gcra(string $key, int $maxBurst, int $requestsPerPeriod, float $period, ?int $numRequests = null)
|
||||
* @method $this get($key)
|
||||
* @method $this getbit($key, $offset)
|
||||
* @method $this getex(string $key, $modifier = '', $value = false)
|
||||
|
||||
@@ -152,6 +152,7 @@ use Predis\Response\Status;
|
||||
* @method array ftsyndump(string $index)
|
||||
* @method Status ftsynupdate(string $index, string $synonymGroupId, ?SynUpdateArguments $arguments = null, string ...$terms)
|
||||
* @method array fttagvals(string $index, string $fieldName)
|
||||
* @method array gcra(string $key, int $maxBurst, int $requestsPerPeriod, float $period, ?int $numRequests = null)
|
||||
* @method string|null get(string $key)
|
||||
* @method int getbit(string $key, $offset)
|
||||
* @method int|null getex(string $key, $modifier = '', $value = false)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2026 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\PrefixableCommand as RedisCommand;
|
||||
|
||||
/**
|
||||
* @see https://redis.io/commands/gcra/
|
||||
*
|
||||
* Rate limit via GCRA. requests_per_period are allowed per period (in seconds)
|
||||
* at a sustained rate. max_burst allows for occasional spikes by granting up to
|
||||
* max_burst additional requests to be consumed at once.
|
||||
*/
|
||||
class GCRA extends RedisCommand
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $responseSchema = [
|
||||
'limited',
|
||||
'maxRequests',
|
||||
'availableRequests',
|
||||
'retryAfter',
|
||||
'fullBurstAfter',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return 'GCRA';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setArguments(array $arguments)
|
||||
{
|
||||
$processedArguments = array_slice($arguments, 0, 4);
|
||||
|
||||
// NUM_REQUESTS option
|
||||
if (isset($arguments[4]) && $arguments[4] !== null) {
|
||||
array_push($processedArguments, 'NUM_REQUESTS', $arguments[4]);
|
||||
}
|
||||
|
||||
parent::setArguments($processedArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parseResponse($data)
|
||||
{
|
||||
return array_combine($this->responseSchema, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parseResp3Response($data)
|
||||
{
|
||||
return $this->parseResponse($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prefixKeys($prefix)
|
||||
{
|
||||
$this->applyPrefixForFirstArgument($prefix);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2026 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\PrefixableCommand;
|
||||
|
||||
/**
|
||||
* @group commands
|
||||
* @group realm-ratelimit
|
||||
*/
|
||||
class GCRA_Test extends PredisCommandTestCase
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCommand(): string
|
||||
{
|
||||
return 'Predis\Command\Redis\GCRA';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedId(): string
|
||||
{
|
||||
return 'GCRA';
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider argumentsProvider
|
||||
* @group disconnected
|
||||
*/
|
||||
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
|
||||
{
|
||||
$raw = [0, 5, 4, -1, 10];
|
||||
$expected = [
|
||||
'limited' => 0,
|
||||
'maxRequests' => 5,
|
||||
'availableRequests' => 4,
|
||||
'retryAfter' => -1,
|
||||
'fullBurstAfter' => 10,
|
||||
];
|
||||
$command = $this->getCommand();
|
||||
|
||||
$this->assertSame($expected, $command->parseResponse($raw));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testPrefixKeys(): void
|
||||
{
|
||||
/** @var PrefixableCommand $command */
|
||||
$command = $this->getCommand();
|
||||
$actualArguments = ['mykey', 4, 10, 1.0];
|
||||
$prefix = 'prefix:';
|
||||
$expectedArguments = ['prefix:mykey', 4, 10, 1.0];
|
||||
|
||||
$command->setArguments($actualArguments);
|
||||
$command->prefixKeys($prefix);
|
||||
|
||||
$this->assertSame($expectedArguments, $command->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.8.0
|
||||
*/
|
||||
public function testRequiredArgumentsDrainAllowanceByOne(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
// max_burst=4, requests_per_period=10, period=10 => 10 req/10s, emission_interval=1s
|
||||
// With max_burst=4, max-req-num = 4+1 = 5
|
||||
$result = $redis->gcra('ratelimit:user1', 4, 10, 10);
|
||||
$expectedResult = [
|
||||
'limited' => 0,
|
||||
'maxRequests' => 5,
|
||||
'availableRequests' => 4,
|
||||
'retryAfter' => -1,
|
||||
'fullBurstAfter' => 1,
|
||||
];
|
||||
|
||||
// First request: not limited
|
||||
$this->assertSame($expectedResult, $result);
|
||||
|
||||
$result = $redis->gcra('ratelimit:user1', 4, 10, 10);
|
||||
$expectedResult = [
|
||||
'limited' => 0,
|
||||
'maxRequests' => 5,
|
||||
'availableRequests' => 3,
|
||||
'retryAfter' => -1,
|
||||
'fullBurstAfter' => 2,
|
||||
];
|
||||
|
||||
// Second request: allowance drained by 1
|
||||
$this->assertSame($expectedResult, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.8.0
|
||||
*/
|
||||
public function testRequiredArgumentsDrainAllowanceByOneResp3(): void
|
||||
{
|
||||
$redis = $this->getResp3Client();
|
||||
|
||||
// max_burst=4, requests_per_period=10, period=10 => 10 req/10s, emission_interval=1s
|
||||
// With max_burst=4, max-req-num = 4+1 = 5
|
||||
$result = $redis->gcra('ratelimit:user1', 4, 10, 10);
|
||||
$expectedResult = [
|
||||
'limited' => 0,
|
||||
'maxRequests' => 5,
|
||||
'availableRequests' => 4,
|
||||
'retryAfter' => -1,
|
||||
'fullBurstAfter' => 1,
|
||||
];
|
||||
|
||||
// First request: not limited
|
||||
$this->assertSame($expectedResult, $result);
|
||||
|
||||
$result = $redis->gcra('ratelimit:user1', 4, 10, 10);
|
||||
$expectedResult = [
|
||||
'limited' => 0,
|
||||
'maxRequests' => 5,
|
||||
'availableRequests' => 3,
|
||||
'retryAfter' => -1,
|
||||
'fullBurstAfter' => 2,
|
||||
];
|
||||
|
||||
// Second request: allowance drained by 1
|
||||
$this->assertSame($expectedResult, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.8.0
|
||||
*/
|
||||
public function testRequestsPerPeriodAndPeriodCombination(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
// max_burst=0, requests_per_period=1, period=60
|
||||
// Only 1 request allowed per 60 seconds, no burst
|
||||
// max-req-num = 0+1 = 1
|
||||
$result1 = $redis->gcra('ratelimit:strict', 0, 1, 60);
|
||||
$expectedResult1 = [
|
||||
'limited' => 0,
|
||||
'maxRequests' => 1,
|
||||
'availableRequests' => 0,
|
||||
'retryAfter' => -1,
|
||||
'fullBurstAfter' => 60,
|
||||
];
|
||||
|
||||
// First request: not limited
|
||||
$this->assertSame($expectedResult1, $result1);
|
||||
|
||||
// Second request is limited (no burst, only 1/60s allowed)
|
||||
$result2 = $redis->gcra('ratelimit:strict', 0, 1, 60);
|
||||
$this->assertSame(1, $result2['limited']);
|
||||
$this->assertSame(1, $result2['maxRequests']);
|
||||
$this->assertSame(0, $result2['availableRequests']);
|
||||
$this->assertGreaterThan(0, $result2['retryAfter']); // retry-after > 0 (must wait)
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.8.0
|
||||
*/
|
||||
public function testNumRequestsDrainsAllowanceByCount(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
// max_burst=9, requests_per_period=10, period=10 => max-req-num = 10
|
||||
// Use NUM_REQUESTS=3 to drain 3 at once
|
||||
$result1 = $redis->gcra('ratelimit:bulk', 9, 10, 10, 3);
|
||||
$expectedResult1 = [
|
||||
'limited' => 0,
|
||||
'maxRequests' => 10,
|
||||
'availableRequests' => 7,
|
||||
'retryAfter' => -1,
|
||||
'fullBurstAfter' => 3,
|
||||
];
|
||||
|
||||
// First request: not limited, 7 available (10 - 3)
|
||||
$this->assertSame($expectedResult1, $result1);
|
||||
|
||||
// Another request with NUM_REQUESTS=3
|
||||
$result2 = $redis->gcra('ratelimit:bulk', 9, 10, 10, 3);
|
||||
$expectedResult2 = [
|
||||
'limited' => 0,
|
||||
'maxRequests' => 10,
|
||||
'availableRequests' => 4,
|
||||
'retryAfter' => -1,
|
||||
'fullBurstAfter' => 6,
|
||||
];
|
||||
|
||||
// Second request: not limited, 4 available (7 - 3)
|
||||
$this->assertSame($expectedResult2, $result2);
|
||||
|
||||
// Another request with NUM_REQUESTS=3
|
||||
$result3 = $redis->gcra('ratelimit:bulk', 9, 10, 10, 3);
|
||||
$expectedResult3 = [
|
||||
'limited' => 0,
|
||||
'maxRequests' => 10,
|
||||
'availableRequests' => 1,
|
||||
'retryAfter' => -1,
|
||||
'fullBurstAfter' => 9,
|
||||
];
|
||||
|
||||
// Third request: not limited, 1 available (4 - 3)
|
||||
$this->assertSame($expectedResult3, $result3);
|
||||
|
||||
// Next request with NUM_REQUESTS=3 should be limited (only 1 left)
|
||||
$result4 = $redis->gcra('ratelimit:bulk', 9, 10, 10, 3);
|
||||
$this->assertSame(1, $result4['limited']);
|
||||
$this->assertGreaterThan(0, $result4['retryAfter']); // must wait
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.8.0
|
||||
*/
|
||||
public function testRetryAfterBehaviorAllowsRequestsAfterWaiting(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
// max_burst=2, requests_per_period=3, period=3 => 3 req/3s, emission_interval=1s
|
||||
// max-req-num = 2+1 = 3
|
||||
// Drain all allowance with 3 requests
|
||||
$redis->gcra('ratelimit:retry', 2, 3, 3);
|
||||
$redis->gcra('ratelimit:retry', 2, 3, 3);
|
||||
$redis->gcra('ratelimit:retry', 2, 3, 3);
|
||||
|
||||
// Next request should be limited
|
||||
$limitedResult = $redis->gcra('ratelimit:retry', 2, 3, 3);
|
||||
$this->assertSame(1, $limitedResult['limited']);
|
||||
$this->assertGreaterThan(0, $limitedResult['retryAfter']);
|
||||
|
||||
// Sleep for retryAfter seconds
|
||||
$retryAfterSeconds = $limitedResult['retryAfter'];
|
||||
sleep($retryAfterSeconds);
|
||||
|
||||
// After waiting, request should no longer be limited
|
||||
$afterWaitResult = $redis->gcra('ratelimit:retry', 2, 3, 3);
|
||||
$this->assertSame(0, $afterWaitResult['limited']);
|
||||
$this->assertSame(-1, $afterWaitResult['retryAfter']);
|
||||
}
|
||||
|
||||
public function argumentsProvider(): array
|
||||
{
|
||||
return [
|
||||
'with required arguments only' => [
|
||||
['mykey', 4, 10, 1.0],
|
||||
['mykey', 4, 10, 1.0],
|
||||
],
|
||||
'with NUM_REQUESTS' => [
|
||||
['mykey', 4, 10, 1.0, 5],
|
||||
['mykey', 4, 10, 1.0, 'NUM_REQUESTS', 5],
|
||||
],
|
||||
'with null NUM_REQUESTS (omitted)' => [
|
||||
['mykey', 4, 10, 1.0, null],
|
||||
['mykey', 4, 10, 1.0],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user