Introduce Redis::OPT_PACK_IGNORE_NUMBERS option.

Adds an option that instructs PhpRedis to not serialize or compress
numeric values. Specifically where `Z_TYPE_P(z) == IS_LONG` or
`Z_TYPE_P(z) == IS_DOUBLE`.

This flag lets the user enable serialization and/or compression while
still using the various increment/decrement command (`INCR`, `INCRBY`,
`DECR`, `DECRBY`, `INCRBYFLOAT`, `HINCRBY`, and `HINCRBYFLOAT`).

Because PhpRedis can't be certain that this option was enabled when
writing keys, there is a small runtime cost on the read-side that tests
whether or not the value its reading is a pure integer or floating point
value.

See #23
This commit is contained in:
michael-grunder
2025-01-26 13:39:42 -08:00
committed by Michael Grunder
parent 41e114177a
commit f9ce9429ef
7 changed files with 189 additions and 29 deletions

View File

@@ -91,20 +91,21 @@ typedef enum _PUBSUB_TYPE {
#define REDIS_SUBS_BUCKETS 3
/* options */
#define REDIS_OPT_SERIALIZER 1
#define REDIS_OPT_PREFIX 2
#define REDIS_OPT_READ_TIMEOUT 3
#define REDIS_OPT_SCAN 4
#define REDIS_OPT_FAILOVER 5
#define REDIS_OPT_TCP_KEEPALIVE 6
#define REDIS_OPT_COMPRESSION 7
#define REDIS_OPT_REPLY_LITERAL 8
#define REDIS_OPT_COMPRESSION_LEVEL 9
#define REDIS_OPT_NULL_MBULK_AS_NULL 10
#define REDIS_OPT_MAX_RETRIES 11
#define REDIS_OPT_BACKOFF_ALGORITHM 12
#define REDIS_OPT_BACKOFF_BASE 13
#define REDIS_OPT_BACKOFF_CAP 14
#define REDIS_OPT_SERIALIZER 1
#define REDIS_OPT_PREFIX 2
#define REDIS_OPT_READ_TIMEOUT 3
#define REDIS_OPT_SCAN 4
#define REDIS_OPT_FAILOVER 5
#define REDIS_OPT_TCP_KEEPALIVE 6
#define REDIS_OPT_COMPRESSION 7
#define REDIS_OPT_REPLY_LITERAL 8
#define REDIS_OPT_COMPRESSION_LEVEL 9
#define REDIS_OPT_NULL_MBULK_AS_NULL 10
#define REDIS_OPT_MAX_RETRIES 11
#define REDIS_OPT_BACKOFF_ALGORITHM 12
#define REDIS_OPT_BACKOFF_BASE 13
#define REDIS_OPT_BACKOFF_CAP 14
#define REDIS_OPT_PACK_IGNORE_NUMBERS 15
/* cluster options */
#define REDIS_FAILOVER_NONE 0
@@ -300,6 +301,7 @@ typedef struct {
zend_string *persistent_id;
HashTable *subs[REDIS_SUBS_BUCKETS];
redis_serializer serializer;
zend_bool pack_ignore_numbers;
int compression;
int compression_level;
long dbNumber;

View File

@@ -3831,12 +3831,38 @@ redis_uncompress(RedisSock *redis_sock, char **dst, size_t *dstlen, const char *
return 0;
}
static int serialize_generic_zval(char **dst, size_t *len, zval *zsrc) {
zend_string *zstr;
zstr = zval_get_string_func(zsrc);
if (ZSTR_IS_INTERNED(zstr)) {
*dst = ZSTR_VAL(zstr);
*len = ZSTR_LEN(zstr);
return 0;
}
*dst = estrndup(ZSTR_VAL(zstr), ZSTR_LEN(zstr));
*len = ZSTR_LEN(zstr);
zend_string_release(zstr);
return 1;
}
PHP_REDIS_API int
redis_pack(RedisSock *redis_sock, zval *z, char **val, size_t *val_len) {
size_t tmplen;
int tmpfree;
char *tmp;
/* Don't pack actual numbers if the user asked us not to */
if (UNEXPECTED(redis_sock->pack_ignore_numbers &&
(Z_TYPE_P(z) == IS_LONG || Z_TYPE_P(z) == IS_DOUBLE)))
{
return serialize_generic_zval(val, val_len, z);
}
/* First serialize */
tmpfree = redis_serialize(redis_sock, z, &tmp, &tmplen);
@@ -3851,9 +3877,29 @@ redis_pack(RedisSock *redis_sock, zval *z, char **val, size_t *val_len) {
PHP_REDIS_API int
redis_unpack(RedisSock *redis_sock, const char *src, int srclen, zval *zdst) {
zend_long lval;
double dval;
size_t len;
char *buf;
if (UNEXPECTED((redis_sock->serializer != REDIS_SERIALIZER_NONE &&
redis_sock->compression != REDIS_COMPRESSION_NONE) &&
redis_sock->pack_ignore_numbers) &&
srclen > 0 && srclen < 24)
{
switch (is_numeric_string(src, srclen, &lval, &dval, 0)) {
case IS_LONG:
ZVAL_LONG(zdst, lval);
return 1;
case IS_DOUBLE:
ZVAL_DOUBLE(zdst, dval);
return 1;
default:
/* Fallthrough */
break;
}
}
/* Uncompress, then unserialize */
if (redis_uncompress(redis_sock, &buf, &len, src, srclen)) {
if (!redis_unserialize(redis_sock, buf, len, zdst)) {
@@ -3898,18 +3944,8 @@ redis_serialize(RedisSock *redis_sock, zval *z, char **val, size_t *val_len)
*val_len = 5;
break;
default: { /* copy */
zend_string *zstr = zval_get_string_func(z);
if (ZSTR_IS_INTERNED(zstr)) { // do not reallocate interned strings
*val = ZSTR_VAL(zstr);
*val_len = ZSTR_LEN(zstr);
return 0;
}
*val = estrndup(ZSTR_VAL(zstr), ZSTR_LEN(zstr));
*val_len = ZSTR_LEN(zstr);
zend_string_efree(zstr);
return 1;
}
default:
return serialize_generic_zval(val, val_len, z);
}
break;
case REDIS_SERIALIZER_PHP:

View File

@@ -151,6 +151,13 @@ class Redis {
*/
public const OPT_NULL_MULTIBULK_AS_NULL = UNKNOWN;
/**
* @var int
* @cvalue REDIS_OPT_PACK_IGNORE_NUMBERS
*
*/
public const OPT_PACK_IGNORE_NUMBERS = UNKNOWN;
/**
*
* @var int

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 1f8f22ab9cd1635066463b20ab12d295c11b4ac7 */
* Stub hash: 78283cf59cefb411c09adf7a0f0bd234c65327b3 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null")
@@ -1817,6 +1817,12 @@ static zend_class_entry *register_class_Redis(void)
zend_declare_class_constant_ex(class_entry, const_OPT_NULL_MULTIBULK_AS_NULL_name, &const_OPT_NULL_MULTIBULK_AS_NULL_value, ZEND_ACC_PUBLIC, NULL);
zend_string_release(const_OPT_NULL_MULTIBULK_AS_NULL_name);
zval const_OPT_PACK_IGNORE_NUMBERS_value;
ZVAL_LONG(&const_OPT_PACK_IGNORE_NUMBERS_value, REDIS_OPT_PACK_IGNORE_NUMBERS);
zend_string *const_OPT_PACK_IGNORE_NUMBERS_name = zend_string_init_interned("OPT_PACK_IGNORE_NUMBERS", sizeof("OPT_PACK_IGNORE_NUMBERS") - 1, 1);
zend_declare_class_constant_ex(class_entry, const_OPT_PACK_IGNORE_NUMBERS_name, &const_OPT_PACK_IGNORE_NUMBERS_value, ZEND_ACC_PUBLIC, NULL);
zend_string_release(const_OPT_PACK_IGNORE_NUMBERS_name);
zval const_SERIALIZER_NONE_value;
ZVAL_LONG(&const_SERIALIZER_NONE_value, REDIS_SERIALIZER_NONE);
zend_string *const_SERIALIZER_NONE_name = zend_string_init_interned("SERIALIZER_NONE", sizeof("SERIALIZER_NONE") - 1, 1);

View File

@@ -6147,6 +6147,8 @@ void redis_getoption_handler(INTERNAL_FUNCTION_PARAMETERS,
RETURN_LONG(redis_sock->compression);
case REDIS_OPT_COMPRESSION_LEVEL:
RETURN_LONG(redis_sock->compression_level);
case REDIS_OPT_PACK_IGNORE_NUMBERS:
RETURN_BOOL(redis_sock->pack_ignore_numbers);
case REDIS_OPT_PREFIX:
if (redis_sock->prefix) {
RETURN_STRINGL(ZSTR_VAL(redis_sock->prefix), ZSTR_LEN(redis_sock->prefix));
@@ -6235,6 +6237,9 @@ void redis_setoption_handler(INTERNAL_FUNCTION_PARAMETERS,
RETURN_TRUE;
}
break;
case REDIS_OPT_PACK_IGNORE_NUMBERS:
redis_sock->pack_ignore_numbers = zval_is_true(val);
RETURN_TRUE;
case REDIS_OPT_COMPRESSION_LEVEL:
val_long = zval_get_long(val);
redis_sock->compression_level = val_long;

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 1f8f22ab9cd1635066463b20ab12d295c11b4ac7 */
* Stub hash: 78283cf59cefb411c09adf7a0f0bd234c65327b3 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0)
ZEND_ARG_INFO(0, options)
@@ -1660,6 +1660,12 @@ static zend_class_entry *register_class_Redis(void)
zend_declare_class_constant_ex(class_entry, const_OPT_NULL_MULTIBULK_AS_NULL_name, &const_OPT_NULL_MULTIBULK_AS_NULL_value, ZEND_ACC_PUBLIC, NULL);
zend_string_release(const_OPT_NULL_MULTIBULK_AS_NULL_name);
zval const_OPT_PACK_IGNORE_NUMBERS_value;
ZVAL_LONG(&const_OPT_PACK_IGNORE_NUMBERS_value, REDIS_OPT_PACK_IGNORE_NUMBERS);
zend_string *const_OPT_PACK_IGNORE_NUMBERS_name = zend_string_init_interned("OPT_PACK_IGNORE_NUMBERS", sizeof("OPT_PACK_IGNORE_NUMBERS") - 1, 1);
zend_declare_class_constant_ex(class_entry, const_OPT_PACK_IGNORE_NUMBERS_name, &const_OPT_PACK_IGNORE_NUMBERS_value, ZEND_ACC_PUBLIC, NULL);
zend_string_release(const_OPT_PACK_IGNORE_NUMBERS_name);
zval const_SERIALIZER_NONE_value;
ZVAL_LONG(&const_SERIALIZER_NONE_value, REDIS_SERIALIZER_NONE);
zend_string *const_SERIALIZER_NONE_name = zend_string_init_interned("SERIALIZER_NONE", sizeof("SERIALIZER_NONE") - 1, 1);

View File

@@ -76,7 +76,7 @@ class Redis_Test extends TestSuite {
$info = $this->redis->info();
$this->version = (isset($info['redis_version'])?$info['redis_version']:'0.0.0');
$this->is_keydb = $this->detectKeyDB($info);
$this->is_valkey = $this->detectValKey($info);
$this->is_valkey = $this->detectValKey($info);
}
protected function minVersionCheck($version) {
@@ -4958,6 +4958,104 @@ class Redis_Test extends TestSuite {
$this->redis->setOption(Redis::OPT_PREFIX, '');
}
private function cartesianProduct(array $arrays) {
$result = [[]];
foreach ($arrays as $array) {
$append = [];
foreach ($result as $product) {
foreach ($array as $item) {
$newProduct = $product;
$newProduct[] = $item;
$append[] = $newProduct;
}
}
$result = $append;
}
return $result;
}
public function testIgnoreNumbers() {
$combinations = $this->cartesianProduct([
[false, true, false],
$this->getSerializers(),
$this->getCompressors(),
]);
foreach ($combinations as [$ignore, $serializer, $compression]) {
$this->redis->setOption(Redis::OPT_PACK_IGNORE_NUMBERS, $ignore);
$this->redis->setOption(Redis::OPT_SERIALIZER, $serializer);
$this->redis->setOption(Redis::OPT_COMPRESSION, $compression);
$this->assertIsInt($this->redis->del('answer'));
$this->assertIsInt($this->redis->del('hash'));
$transparent = $compression === Redis::COMPRESSION_NONE &&
($serializer === Redis::SERIALIZER_NONE ||
$serializer === Redis::SERIALIZER_JSON);
if ($transparent || $ignore) {
$expected_answer = 42;
$expected_pi = 3.14;
} else {
$expected_answer = false;
$expected_pi = false;
}
$this->assertTrue($this->redis->set('answer', 32));
$this->assertEquals($expected_answer, $this->redis->incr('answer', 10));
$this->assertTrue($this->redis->set('pi', 3.04));
$this->assertEquals($expected_pi, $this->redis->incrByFloat('pi', 0.1));
$this->assertEquals(1, $this->redis->hset('hash', 'answer', 32));
$this->assertEquals($expected_answer, $this->redis->hIncrBy('hash', 'answer', 10));
$this->assertEquals(1, $this->redis->hset('hash', 'pi', 3.04));
$this->assertEquals($expected_pi, $this->redis->hIncrByFloat('hash', 'pi', 0.1));
}
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
$this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_NONE);
$this->redis->setOption(Redis::OPT_PACK_IGNORE_NUMBERS, false);
}
function testIgnoreNumbersReturnTypes() {
$combinations = $this->cartesianProduct([
[false, true],
array_filter($this->getSerializers(), function($s) {
return $s !== Redis::SERIALIZER_NONE;
}),
array_filter($this->getCompressors(), function($c) {
return $c !== Redis::COMPRESSION_NONE;
}),
]);
foreach ($combinations as [$ignore, $serializer, $compression]) {
$this->redis->setOption(Redis::OPT_PACK_IGNORE_NUMBERS, $ignore);
$this->redis->setOption(Redis::OPT_SERIALIZER, $serializer);
$this->redis->setOption(Redis::OPT_COMPRESSION, $compression);
foreach ([42, 3.14] as $value) {
$this->assertTrue($this->redis->set('key', $value));
/* There's a known issue in the PHP JSON parser, which
can stringify numbers. Unclear the root cause */
if ($serializer == Redis::SERIALIZER_JSON) {
$this->assertEqualsWeak($value, $this->redis->get('key'));
} else {
$this->assertEquals($value, $this->redis->get('key'));
}
}
}
$this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE);
$this->redis->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_NONE);
$this->redis->setOption(Redis::OPT_PACK_IGNORE_NUMBERS, false);
}
public function testSerializerIGBinary() {
if ( ! defined('Redis::SERIALIZER_IGBINARY'))
$this->markTestSkipped('Redis::SERIALIZER_IGBINARY is not defined');