Compare commits

..

5 Commits

Author SHA1 Message Date
Sergey Sannikov 3ffaf10e99 Return NAN for RESP3 NaN double payloads (#1718)
parseDouble() fell through to a float cast for ',nan', and
(float) 'nan' evaluates to 0.0 in PHP, so a NaN score silently became
a valid zero. Return NAN instead. The RESP3 specification also notes
that Redis before 7.2 may emit any libc representation of NaN
('-nan', 'NAN', 'nan(char-sequence)') and that clients should handle
them, so those spellings are accepted as well.

The TDigest RESP3 tests asserted 0 or null for empty sketches, which
only passed because of the collapsed 0.0 (null == 0.0 loosely); their
RESP2 counterparts already assert the string 'nan' for the same
replies. They now assert NaN.

Note for reviewers: json_encode() throws on NAN, so consumers who
serialize raw replies must handle it; the previous behavior hid NaN
behind a plausible-looking 0.0 instead.
2026-09-10 15:29:52 -07:00
Lazizbek Ergashev e7b89c14b7 Fixed client_info connection parameter being ignored (#1722)
* Fixed `client_info` connection parameter being ignored
* Documented the default value of the `client_info` parameter
2026-09-10 15:24:31 -07:00
dependabot[bot] 3a8c350f3d Bump the github-actions group with 2 updates (#1720) 2026-09-01 16:46:54 -04:00
Sergey Sannikov 675951360f Fix RESP3 double parsing returning positive INF for -inf (#1716)
parseDouble() returned positive INF for the RESP3 payload ',-inf',
inverting the sign. The value is reachable with protocol=3, for
example ZSCORE on a member whose score is -inf.

The existing infinity test asserted only is_infinite(), which cannot
detect the sign inversion, so it now checks exact values.

NaN payloads keep their current behavior; that is addressed
separately.
2026-08-27 16:46:39 +03:00
Till Krüss 0795d69d9e bump dev version 2026-08-14 16:09:00 -07:00
17 changed files with 102 additions and 30 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v7
- name: Check Spelling
uses: rojopolis/spellcheck-github-actions@0.63.0
uses: rojopolis/spellcheck-github-actions@0.66.0
with:
config_path: .github/spellcheck-settings.yml
task_name: Markdown
+5 -5
View File
@@ -85,13 +85,13 @@ jobs:
uses: actions/checkout@v7
- name: Start Redis standalone image
uses: hoverkraft-tech/compose-action@v3.0.0
uses: hoverkraft-tech/compose-action@v3.1.0
with:
compose-file: .github/docker-compose.yml
services: ${{ env.DOCKER_SERVICE }}
- name: Start Redis unprotected image
uses: hoverkraft-tech/compose-action@v3.0.0
uses: hoverkraft-tech/compose-action@v3.1.0
if: ${{ matrix.redis > '4.0' }}
with:
compose-file: .github/docker-compose.yml
@@ -99,7 +99,7 @@ jobs:
- name: Start Redis stack image
id: stack_infra
uses: hoverkraft-tech/compose-action@v3.0.0
uses: hoverkraft-tech/compose-action@v3.1.0
if: ${{ matrix.redis >= '7.2' && matrix.redis < '8.0' }}
with:
compose-file: .github/docker-compose.yml
@@ -107,7 +107,7 @@ jobs:
- name: Start Redis cluster image
id: cluster_infra
uses: hoverkraft-tech/compose-action@v3.0.0
uses: hoverkraft-tech/compose-action@v3.1.0
if: ${{ matrix.redis > '4.0' }}
with:
compose-file: .github/docker-compose.yml
@@ -115,7 +115,7 @@ jobs:
- name: Start Redis sentinels image
id: sentinel_infra
uses: hoverkraft-tech/compose-action@v3.0.0
uses: hoverkraft-tech/compose-action@v3.1.0
if: ${{ matrix.redis > '4.0' }}
with:
compose-file: .github/docker-compose.yml
+4
View File
@@ -1,5 +1,9 @@
## Changelog
## Unreleased
### Changed
- Changed RESP3 double parsing to return `NAN` for NaN payloads instead of `0.0`
## v3.6.0 (2026-08-14)
### Added
- Added support for new TS commands + Indonesian language support integration test (#1695)
+1 -1
View File
@@ -1 +1 @@
3.6.0
3.6.1-dev
+1 -1
View File
@@ -56,7 +56,7 @@ use Traversable;
*/
class Client implements ClientInterface, IteratorAggregate
{
public const VERSION = '3.6.0';
public const VERSION = '3.6.1-dev';
/** @var OptionsInterface */
private $options;
+8 -6
View File
@@ -208,13 +208,15 @@ class Factory implements FactoryInterface
);
}
$connection->addConnectCommand(
new RawCommand('CLIENT', ['SETINFO', 'LIB-NAME', $this->buildLibraryName()])
);
if ($parameters->client_info ?? true) {
$connection->addConnectCommand(
new RawCommand('CLIENT', ['SETINFO', 'LIB-NAME', $this->buildLibraryName()])
);
$connection->addConnectCommand(
new RawCommand('CLIENT', ['SETINFO', 'LIB-VER', Client::VERSION])
);
$connection->addConnectCommand(
new RawCommand('CLIENT', ['SETINFO', 'LIB-VER', Client::VERSION])
);
}
if (isset($parameters->database) && strlen($parameters->database)) {
$connection->addConnectCommand(
+1 -1
View File
@@ -36,7 +36,7 @@ use Predis\Retry\Retry;
* @property string $database Database index (see the SELECT command).
* @property bool $async_connect Performs the connect() operation asynchronously.
* @property bool $tcp_nodelay Toggles the Nagle's algorithm for coalescing.
* @property bool $client_info Whether to set LIB-NAME and LIB-VER when connecting.
* @property bool $client_info Whether to set LIB-NAME and LIB-VER when connecting, enabled by default.
* @property Retry $retry Retry configuration
* @property bool $cache (Relay only) Whether to use in-memory caching.
* @property string $serializer (Relay only) Serializer used for data serialization.
@@ -63,10 +63,18 @@ class Resp3Strategy extends Resp2Strategy
*/
protected function parseDouble(string $string): float
{
if ($string === 'inf' || $string === '-inf') {
if ($string === 'inf') {
return INF;
}
if ($string === '-inf') {
return -INF;
}
if (preg_match('/^-?nan(\(.*\))?$/i', $string) === 1) {
return NAN;
}
return (float) $string;
}
@@ -118,7 +118,11 @@ class TDIGESTBYRANK_Test extends PredisCommandTestCase
$actualResponse = $redis->tdigestbyrank('key', 0, 1, 2, 3, 4, 5, 6);
$this->assertEquals($expectedResponse, $actualResponse);
$this->assertEquals([null, null], $redis->tdigestbyrank('empty_key', 0, 1));
$emptyResponse = $redis->tdigestbyrank('empty_key', 0, 1);
$this->assertCount(2, $emptyResponse);
foreach ($emptyResponse as $value) {
$this->assertNan($value);
}
}
/**
@@ -118,7 +118,11 @@ class TDIGESTBYREVRANK_Test extends PredisCommandTestCase
$actualResponse = $redis->tdigestbyrevrank('key', 0, 1, 2, 3, 4, 5, 6);
$this->assertEquals($expectedResponse, $actualResponse);
$this->assertEquals([null, null], $redis->tdigestbyrevrank('empty_key', 0, 1));
$emptyResponse = $redis->tdigestbyrevrank('empty_key', 0, 1);
$this->assertCount(2, $emptyResponse);
foreach ($emptyResponse as $value) {
$this->assertNan($value);
}
}
/**
@@ -118,7 +118,11 @@ class TDIGESTCDF_Test extends PredisCommandTestCase
$actualResponse = $redis->tdigestcdf('key', 0, 1, 2, 3, 4);
$this->assertSameWithPrecision($expectedResponse, $actualResponse, 5);
$this->assertSame([0.0, 0.0], $redis->tdigestcdf('empty_key', 0, 1));
$emptyResponse = $redis->tdigestcdf('empty_key', 0, 1);
$this->assertCount(2, $emptyResponse);
foreach ($emptyResponse as $value) {
$this->assertNan($value);
}
}
/**
@@ -116,7 +116,7 @@ class TDIGESTMAX_Test extends PredisCommandTestCase
$actualResponse = $redis->tdigestmax('key');
$this->assertEquals('5', $actualResponse);
$this->assertEquals(0, $redis->tdigestmax('empty_key'));
$this->assertNan($redis->tdigestmax('empty_key'));
}
/**
@@ -116,7 +116,7 @@ class TDIGESTMIN_Test extends PredisCommandTestCase
$actualResponse = $redis->tdigestmin('key');
$this->assertEquals('1', $actualResponse);
$this->assertEquals(0, $redis->tdigestmin('empty_key'));
$this->assertNan($redis->tdigestmin('empty_key'));
}
/**
@@ -118,7 +118,11 @@ class TDIGESTQUANTILE_Test extends PredisCommandTestCase
$this->assertEquals([1.0, 2.0, 3.0, 3.0, 4.0, 4.0, 4.0, 5.0, 5.0, 5.0, 5.0], $quantileResponse);
$redis->tdigestcreate('empty_key');
$this->assertEquals([null, null], $redis->tdigestquantile('empty_key', 0.0, 0.1));
$emptyResponse = $redis->tdigestquantile('empty_key', 0.0, 0.1);
$this->assertCount(2, $emptyResponse);
foreach ($emptyResponse as $value) {
$this->assertNan($value);
}
}
/**
@@ -129,10 +129,11 @@ class TDIGESTRESET_Test extends PredisCommandTestCase
$this->assertEquals('OK', $actualResponse);
$this->assertSame(500, $info['Compression']);
$this->assertEquals(
[null, null, null, null, null, null],
$redis->tdigestbyrank('key', 0, 1, 2, 3, 4, 5)
);
$resetResponse = $redis->tdigestbyrank('key', 0, 1, 2, 3, 4, 5);
$this->assertCount(6, $resetResponse);
foreach ($resetResponse as $value) {
$this->assertNan($value);
}
}
/**
+18
View File
@@ -585,6 +585,24 @@ class FactoryTest extends PredisTestCase
$this->assertSame(['SETINFO', 'LIB-VER', Client::VERSION], $initCommands[2]->getArguments());
}
/**
* @group disconnected
* @return void
*/
public function testDoesNotSetClientNameAndVersionOnConnectionWithClientInfoDisabled(): void
{
$parameters = ['client_info' => false];
$factory = new Factory();
$connection = $factory->create($parameters);
$initCommands = $connection->getInitCommands();
$this->assertCount(1, $initCommands);
$this->assertInstanceOf(RawCommand::class, $initCommands[0]);
$this->assertSame('HELLO', $initCommands[0]->getId());
$this->assertSame([2, 'SETNAME', 'predis'], $initCommands[0]->getArguments());
}
/**
* @group disconnected
*/
@@ -60,11 +60,24 @@ class Resp3StrategyTest extends PredisTestCase
* @param string $data
* @return void
*/
public function testParseDataReturnsFloatInfinityOnInfinityOrNegativeInfinity(string $data): void
public function testParseDataReturnsFloatInfinityOnInfinityOrNegativeInfinity(string $data, float $expectedValue): void
{
$actualResponse = $this->strategy->parseData($data);
$this->assertInfinite($actualResponse);
$this->assertSame($expectedValue, $actualResponse);
}
/**
* @dataProvider nanProvider
* @group disconnected
* @param string $data
* @return void
*/
public function testParseDataReturnsFloatNanOnNanValue(string $data): void
{
$actualResponse = $this->strategy->parseData($data);
$this->assertNan($actualResponse);
}
/**
@@ -166,8 +179,18 @@ class Resp3StrategyTest extends PredisTestCase
public function infinityProvider(): array
{
return [
'positive infinity' => [",inf\r\n"],
'negative infinity' => [",-inf\r\n"],
'positive infinity' => [",inf\r\n", INF],
'negative infinity' => [",-inf\r\n", -INF],
];
}
public function nanProvider(): array
{
return [
'canonical nan' => [",nan\r\n"],
'negative nan' => [",-nan\r\n"],
'uppercase nan' => [",NAN\r\n"],
'nan with payload' => [",nan(ind)\r\n"],
];
}