summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--php-pecl-redis5.spec43
-rw-r--r--redis-php8.patch658
2 files changed, 695 insertions, 6 deletions
diff --git a/php-pecl-redis5.spec b/php-pecl-redis5.spec
index 7e6563b..e6f5389 100644
--- a/php-pecl-redis5.spec
+++ b/php-pecl-redis5.spec
@@ -21,6 +21,13 @@
%global with_zts 0%{!?_without_zts:%{?__ztsphp:1}}
%bcond_without tests
%bcond_without igbinary
+# NOTICE: this only work when php-devel in buildroot (so only for SCL)
+# use mock... --without msgpack for base package
+%if "%{php_version}" > "8.0"
+%bcond_with msgpack
+%else
+%bcond_without msgpack
+%endif
# after 20-json, 40-igbinary and 40-msgpack
%global ini_name 50-%{pecl_name}.ini
@@ -30,11 +37,13 @@
Summary: Extension for communicating with the Redis key-value store
Name: %{?sub_prefix}php-pecl-redis5
Version: %{upstream_version}%{?upstream_prever:~%{upstream_prever}}
-Release: 1%{?dist}%{!?scl:%{!?nophptag:%(%{__php} -r 'echo ".".PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')}}
+Release: 2%{?dist}%{!?scl:%{!?nophptag:%(%{__php} -r 'echo ".".PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')}}
License: PHP
URL: https://pecl.php.net/package/redis
Source0: https://pecl.php.net/get/%{pecl_name}-%{upstream_version}%{?upstream_prever}.tgz
+Patch0: %{pecl_name}-php8.patch
+
BuildRequires: %{?dtsprefix}gcc
BuildRequires: %{?scl_prefix}php-devel > 7
BuildRequires: %{?scl_prefix}php-pear
@@ -42,7 +51,9 @@ BuildRequires: %{?scl_prefix}php-json
%if %{with igbinary}
BuildRequires: %{?scl_prefix}php-pecl-igbinary-devel
%endif
+%if %{with msgpack}
BuildRequires: %{?scl_prefix}php-pecl-msgpack-devel >= 2.0.3
+%endif
BuildRequires: pkgconfig(liblzf)
BuildRequires: pkgconfig(libzstd) >= 1.3.0
BuildRequires: pkgconfig(liblz4)
@@ -57,7 +68,9 @@ Requires: %{?scl_prefix}php-json%{?_isa}
%if %{with igbinary}
Requires: %{?scl_prefix}php-pecl(igbinary)%{?_isa}
%endif
+%if %{with msgpack}
Requires: %{?scl_prefix}php-pecl-msgpack%{?_isa}
+%endif
%{?_sclreq:Requires: %{?scl_prefix}runtime%{?_sclreq}%{?_isa}}
Obsoletes: %{?scl_prefix}php-%{pecl_name} < 3
@@ -79,7 +92,7 @@ Conflicts: %{?scl_prefix}php-pecl-%{pecl_name} < 5
Conflicts: %{?scl_prefix}php-pecl-%{pecl_name}4 < 5
%endif
-%if "%{?vendor}" == "Remi Collet" && 0%{!?scl:1} && 0%{?rhel}
+%if "%{?packager}" == "Remi Collet" && 0%{!?scl:1} && 0%{?rhel}
# Other third party repo stuff
%if "%{php_version}" > "7.1"
Obsoletes: php71u-pecl-%{pecl_name} <= %{version}
@@ -95,7 +108,9 @@ Obsoletes: php73w-pecl-%{pecl_name} <= %{version}
%endif
%if "%{php_version}" > "7.4"
Obsoletes: php74-pecl-%{pecl_name} <= %{version}
-Obsoletes: php74w-pecl-%{pecl_name} <= %{version}
+%endif
+%if "%{php_version}" > "8.0"
+Obsoletes: php80-pecl-%{pecl_name} <= %{version}
%endif
%endif
@@ -129,6 +144,10 @@ sed -e 's/role="test"/role="src"/' \
-i package.xml
cd NTS
+%if "%{php_version}" > "8.0"
+%patch0 -p1 -b.php8
+%endif
+
# Use system library
rm -r liblzf
@@ -203,7 +222,9 @@ peclconf() {
%if %{with igbinary}
--enable-redis-igbinary \
%endif
+%if %{with msgpack}
--enable-redis-msgpack \
+%endif
--enable-redis-lzf \
--with-liblzf \
--enable-redis-zstd \
@@ -251,20 +272,25 @@ done
%check
# simple module load test
-DEPS="--no-php-ini --define extension=json.so"
+DEPS="--no-php-ini"
+%if "%{php_version}" < "8.0"
+DEPS="$DEPS --define extension=json.so"
+%endif
%if %{with igbinary}
DEPS="$DEPS --define extension=igbinary.so"
%endif
+%if %{with msgpack}
DEPS="$DEPS --define extension=msgpack.so"
+%endif
%{__php} $DEPS \
--define extension=%{buildroot}%{php_extdir}/%{pecl_name}.so \
- --modules | grep %{pecl_name}
+ --modules | grep '^%{pecl_name}$'
%if %{with_zts}
%{__ztsphp} $DEPS \
--define extension=%{buildroot}%{php_ztsextdir}/%{pecl_name}.so \
- --modules | grep %{pecl_name}
+ --modules | grep '^%{pecl_name}$'
%endif
%if %{with tests}
@@ -339,6 +365,11 @@ fi
%changelog
+* Fri Sep 11 2020 Remi Collet <remi@remirepo.net> - 5.3.1-2
+- add patches for PHP 8 from upstream and
+ https://github.com/phpredis/phpredis/pull/1845
+- disable msgpack serializer with PHP 8
+
* Wed Jul 8 2020 Remi Collet <remi@remirepo.net> - 5.3.1-1
- update to 5.3.1
diff --git a/redis-php8.patch b/redis-php8.patch
new file mode 100644
index 0000000..1227349
--- /dev/null
+++ b/redis-php8.patch
@@ -0,0 +1,658 @@
+diff --git a/cluster_library.c b/cluster_library.c
+index 98ba9c2..3720f72 100644
+--- a/cluster_library.c
++++ b/cluster_library.c
+@@ -634,8 +634,12 @@ cluster_node_create(redisCluster *c, char *host, size_t host_len,
+ zend_llist_init(&node->slots, sizeof(redisSlotRange), NULL, 0);
+
+ // Attach socket
+- node->sock = redis_sock_create(host, host_len, port, c->timeout,
+- c->read_timeout, c->persistent, NULL, 0);
++ node->sock = redis_sock_create(host, host_len, port,
++ c->flags->timeout, c->flags->read_timeout,
++ c->flags->persistent, NULL, 0);
++
++ /* Stream context */
++ node->sock->stream_ctx = c->flags->stream_ctx;
+
+ redis_sock_set_auth(node->sock, c->flags->user, c->flags->pass);
+
+@@ -818,12 +822,12 @@ PHP_REDIS_API redisCluster *cluster_create(double timeout, double read_timeout,
+
+ /* Initialize flags and settings */
+ c->flags = ecalloc(1, sizeof(RedisSock));
++ c->flags->timeout = timeout;
++ c->flags->read_timeout = read_timeout;
++ c->flags->persistent = persistent;
+ c->subscribed_slot = -1;
+ c->clusterdown = 0;
+- c->timeout = timeout;
+- c->read_timeout = read_timeout;
+ c->failover = failover;
+- c->persistent = persistent;
+ c->err = NULL;
+
+ /* Set up our waitms based on timeout */
+@@ -993,9 +997,12 @@ void cluster_init_cache(redisCluster *c, redisCachedCluster *cc) {
+
+ /* Create socket */
+ sock = redis_sock_create(ZSTR_VAL(cm->host.addr), ZSTR_LEN(cm->host.addr), cm->host.port,
+- c->timeout, c->read_timeout, c->persistent,
++ c->flags->timeout, c->flags->read_timeout, c->flags->persistent,
+ NULL, 0);
+
++ /* Stream context */
++ sock->stream_ctx = c->flags->stream_ctx;
++
+ /* Add to seed nodes */
+ zend_hash_str_update_ptr(c->seeds, key, keylen, sock);
+
+@@ -1027,7 +1034,8 @@ void cluster_init_cache(redisCluster *c, redisCachedCluster *cc) {
+ * seeds array and know we have a non-empty array of strings all in
+ * host:port format. */
+ PHP_REDIS_API void
+-cluster_init_seeds(redisCluster *cluster, zend_string **seeds, uint32_t nseeds) {
++cluster_init_seeds(redisCluster *c, zend_string **seeds, uint32_t nseeds)
++{
+ RedisSock *sock;
+ char *seed, *sep, key[1024];
+ int key_len, i, *map;
+@@ -1044,19 +1052,22 @@ cluster_init_seeds(redisCluster *cluster, zend_string **seeds, uint32_t nseeds)
+ ZEND_ASSERT(sep != NULL);
+
+ // Allocate a structure for this seed
+- sock = redis_sock_create(seed, sep - seed,
+- (unsigned short)atoi(sep+1), cluster->timeout,
+- cluster->read_timeout, cluster->persistent, NULL, 0);
++ sock = redis_sock_create(seed, sep - seed, atoi(sep + 1),
++ c->flags->timeout, c->flags->read_timeout,
++ c->flags->persistent, NULL, 0);
++
++ /* Stream context */
++ sock->stream_ctx = c->flags->stream_ctx;
+
+ /* Credentials */
+- redis_sock_set_auth(sock, cluster->flags->user, cluster->flags->pass);
++ redis_sock_set_auth(sock, c->flags->user, c->flags->pass);
+
+ // Index this seed by host/port
+ key_len = snprintf(key, sizeof(key), "%s:%u", ZSTR_VAL(sock->host),
+ sock->port);
+
+ // Add to our seed HashTable
+- zend_hash_str_update_ptr(cluster->seeds, key, key_len, sock);
++ zend_hash_str_update_ptr(c->seeds, key, key_len, sock);
+ }
+
+ efree(map);
+@@ -1807,7 +1818,6 @@ PHP_REDIS_API void cluster_sub_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *
+ zval z_ret, z_args[4];
+ sctx->cb.retval = &z_ret;
+ sctx->cb.params = z_args;
+- sctx->cb.no_separation = 0;
+
+ /* We're in a subscribe loop */
+ c->subscribed_slot = c->cmd_slot;
+diff --git a/cluster_library.h b/cluster_library.h
+index de9d171..98e9b0e 100644
+--- a/cluster_library.h
++++ b/cluster_library.h
+@@ -186,12 +186,8 @@ typedef struct clusterFoldItem clusterFoldItem;
+ /* RedisCluster implementation structure */
+ typedef struct redisCluster {
+
+- /* Timeout and read timeout (for normal operations) */
+- double timeout;
+- double read_timeout;
+-
+- /* Are we using persistent connections */
+- int persistent;
++ /* One RedisSock struct for serialization and prefix information */
++ RedisSock *flags;
+
+ /* How long in milliseconds should we wait when being bounced around */
+ long waitms;
+@@ -241,9 +237,6 @@ typedef struct redisCluster {
+ /* The slot where we're subscribed */
+ short subscribed_slot;
+
+- /* One RedisSock struct for serialization and prefix information */
+- RedisSock *flags;
+-
+ /* The first line of our last reply, not including our reply type byte
+ * or the trailing \r\n */
+ char line_reply[1024];
+diff --git a/library.c b/library.c
+index 2f95aea..6aef627 100644
+--- a/library.c
++++ b/library.c
+@@ -464,7 +464,6 @@ PHP_REDIS_API int redis_subscribe_response(INTERNAL_FUNCTION_PARAMETERS,
+ zval z_ret, z_args[4];
+ sctx->cb.retval = &z_ret;
+ sctx->cb.params = z_args;
+- sctx->cb.no_separation = 0;
+
+ /* Multibulk response, {[pattern], type, channel, payload } */
+ while(1) {
+@@ -686,19 +685,6 @@ redis_sock_read(RedisSock *redis_sock, int *buf_len)
+ return NULL;
+ }
+
+-static int redis_sock_read_cmp(RedisSock *redis_sock, const char *cmp, int cmplen) {
+- char *resp;
+- int len, rv = FAILURE;
+-
+- if ((resp = redis_sock_read(redis_sock, &len)) == NULL)
+- return FAILURE;
+-
+- rv = len == cmplen && !memcmp(resp, cmp, cmplen) ? SUCCESS : FAILURE;
+-
+- efree(resp);
+- return rv;
+-}
+-
+ /* A simple union to store the various arg types we might handle in our
+ * redis_spprintf command formatting function */
+ union resparg {
+@@ -1519,17 +1505,21 @@ redis_read_stream_messages(RedisSock *redis_sock, int count, zval *z_ret
+ * the multi-bulk header for field and values */
+ if ((read_mbulk_header(redis_sock, &mhdr) < 0 || mhdr != 2) ||
+ ((id = redis_sock_read(redis_sock, &idlen)) == NULL) ||
+- (read_mbulk_header(redis_sock, &fields) < 0 || fields % 2 != 0))
++ (read_mbulk_header(redis_sock, &fields) < 0 ||
++ (fields > 0 && fields % 2 != 0)))
+ {
+ if (id) efree(id);
+ return -1;
+ }
+
+- array_init(&z_message);
+-
+- redis_mbulk_reply_loop(redis_sock, &z_message, fields, UNSERIALIZE_VALS);
+- array_zip_values_and_scores(redis_sock, &z_message, SCORE_DECODE_NONE);
+- add_assoc_zval_ex(z_ret, id, idlen, &z_message);
++ if (fields < 0) {
++ add_assoc_null_ex(z_ret, id, idlen);
++ } else {
++ array_init(&z_message);
++ redis_mbulk_reply_loop(redis_sock, &z_message, fields, UNSERIALIZE_VALS);
++ array_zip_values_and_scores(redis_sock, &z_message, SCORE_DECODE_NONE);
++ add_assoc_zval_ex(z_ret, id, idlen, &z_message);
++ }
+ efree(id);
+ }
+
+@@ -1727,7 +1717,10 @@ redis_read_xinfo_response(RedisSock *redis_sock, zval *z_ret, int elements)
+ switch (type) {
+ case TYPE_BULK:
+ if ((data = redis_sock_read_bulk_reply(redis_sock, li)) == NULL) {
+- goto failure;
++ if (!key) goto failure;
++ add_assoc_null_ex(z_ret, key, len);
++ efree(key);
++ key = NULL;
+ } else if (key) {
+ add_assoc_stringl_ex(z_ret, key, len, data, li);
+ efree(data);
+@@ -2130,16 +2123,21 @@ static int redis_stream_liveness_check(php_stream *stream) {
+ static int
+ redis_sock_check_liveness(RedisSock *redis_sock)
+ {
+- char id[64], ok;
++ char id[64], inbuf[4096];
+ int idlen, auth;
+ smart_string cmd = {0};
++ size_t len;
+
+ /* Short circuit if we detect the stream has gone bad or if the user has
+ * configured persistent connection "YOLO mode". */
+- if (redis_stream_liveness_check(redis_sock->stream) != SUCCESS)
+- return FAILURE;
+- else if (!INI_INT("redis.pconnect.echo_check_liveness"))
++ if (redis_stream_liveness_check(redis_sock->stream) != SUCCESS) {
++ goto failure;
++ }
++
++ redis_sock->status = REDIS_SOCK_STATUS_CONNECTED;
++ if (!INI_INT("redis.pconnect.echo_check_liveness")) {
+ return SUCCESS;
++ }
+
+ /* AUTH (if we need it) */
+ auth = redis_sock_append_auth(redis_sock, &cmd);
+@@ -2150,12 +2148,55 @@ redis_sock_check_liveness(RedisSock *redis_sock)
+ redis_cmd_append_sstr(&cmd, id, idlen);
+
+ /* Send command(s) and make sure we can consume reply(ies) */
+- ok = (redis_sock_write(redis_sock, cmd.c, cmd.len) >= 0) &&
+- (!auth || redis_sock_read_ok(redis_sock) == SUCCESS) &&
+- (redis_sock_read_cmp(redis_sock, id, idlen) == SUCCESS);
+-
++ if (redis_sock_write(redis_sock, cmd.c, cmd.len) < 0) {
++ smart_string_free(&cmd);
++ goto failure;
++ }
+ smart_string_free(&cmd);
+- return ok ? SUCCESS : FAILURE;
++
++ if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0) {
++ goto failure;
++ }
++
++ if (auth) {
++ if (strncmp(inbuf, "+OK", 3) == 0 || strncmp(inbuf, "-ERR Client sent AUTH", 21) == 0) {
++ /* successfully authenticated or authentication isn't required */
++ if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0) {
++ goto failure;
++ }
++ } else if (strncmp(inbuf, "-NOAUTH", 7) == 0) {
++ /* connection is fine but authentication failed, next command must fails too */
++ if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0 || strncmp(inbuf, "-NOAUTH", 7) != 0) {
++ goto failure;
++ }
++ return SUCCESS;
++ } else {
++ goto failure;
++ }
++ redis_sock->status = REDIS_SOCK_STATUS_READY;
++ } else {
++ if (strncmp(inbuf, "-NOAUTH", 7) == 0) {
++ /* connection is fine but authentication required */
++ return SUCCESS;
++ }
++ }
++
++ /* check echo response */
++ if (*inbuf != TYPE_BULK || atoi(inbuf + 1) != idlen ||
++ redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0 ||
++ strncmp(inbuf, id, idlen) != 0
++ ) {
++ goto failure;
++ }
++
++ return SUCCESS;
++failure:
++ redis_sock->status = REDIS_SOCK_STATUS_DISCONNECTED;
++ if (redis_sock->stream) {
++ php_stream_pclose(redis_sock->stream);
++ redis_sock->stream = NULL;
++ }
++ return FAILURE;
+ }
+
+ /**
+@@ -2165,9 +2206,9 @@ PHP_REDIS_API int redis_sock_connect(RedisSock *redis_sock)
+ {
+ struct timeval tv, read_tv, *tv_ptr = NULL;
+ zend_string *persistent_id = NULL, *estr = NULL;
+- char host[1024], *pos, *address, *schema = NULL;
++ char host[1024], *pos, *address, *scheme = NULL;
+ const char *fmtstr = "%s://%s:%d";
+- int host_len, usocket = 0, err = 0, tcp_flag = 1;
++ int host_len, usocket = 0, err = 0, tcp_flag = 1, scheme_free = 0;
+ ConnectionPool *p = NULL;
+
+ if (redis_sock->stream != NULL) {
+@@ -2175,9 +2216,12 @@ PHP_REDIS_API int redis_sock_connect(RedisSock *redis_sock)
+ }
+
+ address = ZSTR_VAL(redis_sock->host);
+- if ((pos = strstr(address, "://")) != NULL) {
+- schema = estrndup(address, pos - address);
++ if ((pos = strstr(address, "://")) == NULL) {
++ scheme = redis_sock->stream_ctx ? "ssl" : "tcp";
++ } else {
++ scheme = estrndup(address, pos - address);
+ address = pos + sizeof("://") - 1;
++ scheme_free = 1;
+ }
+ if (address[0] == '/' && redis_sock->port < 1) {
+ host_len = snprintf(host, sizeof(host), "unix://%s", address);
+@@ -2193,9 +2237,9 @@ PHP_REDIS_API int redis_sock_connect(RedisSock *redis_sock)
+ fmtstr = "%s://[%s]:%d";
+ }
+ #endif
+- host_len = snprintf(host, sizeof(host), fmtstr, schema ? schema : "tcp", address, redis_sock->port);
+- if (schema) efree(schema);
++ host_len = snprintf(host, sizeof(host), fmtstr, scheme, address, redis_sock->port);
+ }
++ if (scheme_free) efree(scheme);
+
+ if (redis_sock->persistent) {
+ if (INI_INT("redis.pconnect.pooling_enabled")) {
+@@ -2205,11 +2249,7 @@ PHP_REDIS_API int redis_sock_connect(RedisSock *redis_sock)
+ zend_llist_remove_tail(&p->list);
+
+ if (redis_sock_check_liveness(redis_sock) == SUCCESS) {
+- redis_sock->status = REDIS_SOCK_STATUS_READY;
+ return SUCCESS;
+- } else if (redis_sock->stream) {
+- php_stream_pclose(redis_sock->stream);
+- redis_sock->stream = NULL;
+ }
+ p->nb_active--;
+ }
+diff --git a/redis_array_impl.c b/redis_array_impl.c
+index a427f67..8d8cece 100644
+--- a/redis_array_impl.c
++++ b/redis_array_impl.c
+@@ -1134,7 +1134,6 @@ zval_rehash_callback(zend_fcall_info *z_cb, zend_fcall_info_cache *z_cb_cache,
+ z_cb->params = z_args;
+ z_cb->retval = z_ret;
+
+- z_cb->no_separation = 0;
+ z_cb->param_count = 2;
+
+ /* run cb(hostname, count) */
+diff --git a/redis_cluster.c b/redis_cluster.c
+index 3233b65..5cb453b 100644
+--- a/redis_cluster.c
++++ b/redis_cluster.c
+@@ -351,7 +351,7 @@ void free_cluster_context(zend_object *object) {
+ /* Attempt to connect to a Redis cluster provided seeds and timeout options */
+ static void redis_cluster_init(redisCluster *c, HashTable *ht_seeds, double timeout,
+ double read_timeout, int persistent, zend_string *user,
+- zend_string *pass)
++ zend_string *pass, zval *context)
+ {
+ zend_string *hash = NULL, **seeds;
+ redisCachedCluster *cc;
+@@ -369,10 +369,13 @@ static void redis_cluster_init(redisCluster *c, HashTable *ht_seeds, double time
+ c->flags->user = zend_string_copy(user);
+ if (pass && ZSTR_LEN(pass))
+ c->flags->pass = zend_string_copy(pass);
++ if (context) {
++ redis_sock_set_stream_context(c->flags, context);
++ }
+
+- c->timeout = timeout;
+- c->read_timeout = read_timeout;
+- c->persistent = persistent;
++ c->flags->timeout = timeout;
++ c->flags->read_timeout = read_timeout;
++ c->flags->persistent = persistent;
+ c->waitms = timeout * 1000L;
+
+ /* Attempt to load slots from cache if caching is enabled */
+@@ -450,7 +453,7 @@ void redis_cluster_load(redisCluster *c, char *name, int name_len) {
+ }
+
+ /* Attempt to create/connect to the cluster */
+- redis_cluster_init(c, ht_seeds, timeout, read_timeout, persistent, user, pass);
++ redis_cluster_init(c, ht_seeds, timeout, read_timeout, persistent, user, pass, NULL);
+
+ /* Clean up */
+ zval_dtor(&z_seeds);
+@@ -464,38 +467,36 @@ void redis_cluster_load(redisCluster *c, char *name, int name_len) {
+
+ /* Create a RedisCluster Object */
+ PHP_METHOD(RedisCluster, __construct) {
+- zval *object, *z_seeds = NULL, *z_auth = NULL;
++ zval *object, *z_seeds = NULL, *z_auth = NULL, *context = NULL;
+ zend_string *user = NULL, *pass = NULL;
+ double timeout = 0.0, read_timeout = 0.0;
+ size_t name_len;
+ zend_bool persistent = 0;
+- redisCluster *context = GET_CONTEXT();
++ redisCluster *c = GET_CONTEXT();
+ char *name;
+
+ // Parse arguments
+ if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(),
+- "Os!|addbz", &object, redis_cluster_ce, &name,
++ "Os!|addbza", &object, redis_cluster_ce, &name,
+ &name_len, &z_seeds, &timeout, &read_timeout,
+- &persistent, &z_auth) == FAILURE)
++ &persistent, &z_auth, &context) == FAILURE)
+ {
+ RETURN_FALSE;
+ }
+
+- // Require a name
+- if (name_len == 0 && ZEND_NUM_ARGS() < 2) {
+- CLUSTER_THROW_EXCEPTION("You must specify a name or pass seeds!", 0);
+- }
+-
+ /* If we've got a string try to load from INI */
+ if (ZEND_NUM_ARGS() < 2) {
+- redis_cluster_load(context, name, name_len);
++ if (name_len == 0) { // Require a name
++ CLUSTER_THROW_EXCEPTION("You must specify a name or pass seeds!", 0);
++ }
++ redis_cluster_load(c, name, name_len);
+ return;
+ }
+
+ /* The normal case, loading from arguments */
+ redis_extract_auth_info(z_auth, &user, &pass);
+- redis_cluster_init(context, Z_ARRVAL_P(z_seeds), timeout, read_timeout,
+- persistent, user, pass);
++ redis_cluster_init(c, Z_ARRVAL_P(z_seeds), timeout, read_timeout,
++ persistent, user, pass, context);
+
+ if (user) zend_string_release(user);
+ if (pass) zend_string_release(pass);
+diff --git a/redis_commands.c b/redis_commands.c
+index 3b19194..d6bb2bc 100644
+--- a/redis_commands.c
++++ b/redis_commands.c
+@@ -793,7 +793,7 @@ int redis_subscribe_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
+ zval *z_arr, *z_chan;
+ HashTable *ht_chan;
+ smart_string cmdstr = {0};
+- subscribeContext *sctx = emalloc(sizeof(subscribeContext));
++ subscribeContext *sctx = ecalloc(1, sizeof(*sctx));
+ size_t key_len;
+ int key_free;
+ char *key;
+@@ -854,7 +854,7 @@ int redis_unsubscribe_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
+ zval *z_arr, *z_chan;
+ HashTable *ht_arr;
+ smart_string cmdstr = {0};
+- subscribeContext *sctx = emalloc(sizeof(subscribeContext));
++ subscribeContext *sctx = ecalloc(1, sizeof(*sctx));
+
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "a", &z_arr) == FAILURE) {
+ efree(sctx);
+@@ -1299,6 +1299,34 @@ int redis_blocking_pop_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
+ * have specific processing (argument validation, etc) that make them unique
+ */
+
++/* Attempt to pull a long expiry from a zval. We're more restrictave than zval_get_long
++ * because that function will return integers from things like open file descriptors
++ * which should simply fail as a TTL */
++static int redis_try_get_expiry(zval *zv, zend_long *lval) {
++ double dval;
++
++ /* Success on an actual long or double */
++ if (Z_TYPE_P(zv) == IS_LONG || Z_TYPE_P(zv) == IS_DOUBLE) {
++ *lval = zval_get_long(zv);
++ return SUCCESS;
++ }
++
++ /* Automatically fail if we're not a string */
++ if (Z_TYPE_P(zv) != IS_STRING)
++ return FAILURE;
++
++ /* Attempt to get a long from the string */
++ switch (is_numeric_string(Z_STRVAL_P(zv), Z_STRLEN_P(zv), lval, &dval, 0)) {
++ case IS_DOUBLE:
++ *lval = dval;
++ /* fallthrough */
++ case IS_LONG:
++ return SUCCESS;
++ default:
++ return FAILURE;
++ }
++}
++
+ /* SET */
+ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
+ char **cmd, int *cmd_len, short *slot, void **ctx)
+@@ -1306,7 +1334,8 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
+ smart_string cmdstr = {0};
+ zval *z_value, *z_opts=NULL;
+ char *key = NULL, *exp_type = NULL, *set_type = NULL;
+- long expire = -1, exp_set = 0, keep_ttl = 0;
++ long exp_set = 0, keep_ttl = 0;
++ zend_long expire = -1;
+ size_t key_len;
+
+ // Make sure the function is being called correctly
+@@ -1316,14 +1345,6 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
+ return FAILURE;
+ }
+
+- /* Our optional argument can either be a long (to support legacy SETEX */
+- /* redirection), or an array with Redis >= 2.6.12 set options */
+- if (z_opts && Z_TYPE_P(z_opts) != IS_LONG && Z_TYPE_P(z_opts) != IS_ARRAY
+- && Z_TYPE_P(z_opts) != IS_NULL)
+- {
+- return FAILURE;
+- }
+-
+ // Check for an options array
+ if (z_opts && Z_TYPE_P(z_opts) == IS_ARRAY) {
+ HashTable *kt = Z_ARRVAL_P(z_opts);
+@@ -1355,8 +1376,12 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
+ set_type = Z_STRVAL_P(v);
+ }
+ } ZEND_HASH_FOREACH_END();
+- } else if (z_opts && Z_TYPE_P(z_opts) == IS_LONG) {
+- expire = Z_LVAL_P(z_opts);
++ } else if (z_opts && Z_TYPE_P(z_opts) != IS_NULL) {
++ if (redis_try_get_expiry(z_opts, &expire) == FAILURE) {
++ php_error_docref(NULL, E_WARNING, "Expire must be a long, double, or a numeric string");
++ return FAILURE;
++ }
++
+ exp_set = 1;
+ }
+
+@@ -1386,7 +1411,7 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
+
+ if (exp_type) {
+ redis_cmd_append_sstr(&cmdstr, exp_type, strlen(exp_type));
+- redis_cmd_append_sstr_long(&cmdstr, expire);
++ redis_cmd_append_sstr_long(&cmdstr, (long)expire);
+ }
+
+ if (set_type)
+diff --git a/redis_session.c b/redis_session.c
+index 6f2c5c5..4165fff 100644
+--- a/redis_session.c
++++ b/redis_session.c
+@@ -854,7 +854,7 @@ static char *cluster_session_key(redisCluster *c, const char *key, int keylen,
+
+ PS_OPEN_FUNC(rediscluster) {
+ redisCluster *c;
+- zval z_conf, *zv;
++ zval z_conf, *zv, *context;
+ HashTable *ht_conf, *ht_seeds;
+ double timeout = 0, read_timeout = 0;
+ int persistent = 0, failover = REDIS_FAILOVER_NONE;
+@@ -907,6 +907,7 @@ PS_OPEN_FUNC(rediscluster) {
+
+ #define CLUSTER_SESSION_CLEANUP() \
+ if (hash) zend_string_release(hash); \
++ if (failstr) zend_string_release(failstr); \
+ if (prefix) zend_string_release(prefix); \
+ if (user) zend_string_release(user); \
+ if (pass) zend_string_release(pass); \
+@@ -918,7 +919,6 @@ PS_OPEN_FUNC(rediscluster) {
+ if (seeds == NULL) {
+ php_error_docref(NULL, E_WARNING, "No valid seeds detected");
+ CLUSTER_SESSION_CLEANUP();
+- zval_dtor(&z_conf);
+ return FAILURE;
+ }
+
+@@ -932,6 +932,10 @@ PS_OPEN_FUNC(rediscluster) {
+
+ redis_sock_set_auth(c->flags, user, pass);
+
++ if ((context = REDIS_HASH_STR_FIND_TYPE_STATIC(ht_conf, "stream", IS_ARRAY)) != NULL) {
++ redis_sock_set_stream_context(c->flags, context);
++ }
++
+ /* First attempt to load from cache */
+ if (CLUSTER_CACHING_ENABLED()) {
+ hash = cluster_hash_seeds(seeds, nseeds);
+diff --git a/tests/RedisTest.php b/tests/RedisTest.php
+index c7a403f..3c84885 100644
+--- a/tests/RedisTest.php
++++ b/tests/RedisTest.php
+@@ -371,9 +371,14 @@ class Redis_Test extends TestSuite
+ $this->assertEquals($this->redis->get('foo'), 'bar');
+ $this->assertEquals($this->redis->ttl('foo'), 20);
+
++ /* Should coerce doubles into long */
++ $this->assertTrue($this->redis->set('foo', 'bar-20.5', 20.5));
++ $this->assertEquals($this->redis->ttl('foo'), 20);
++ $this->assertEquals($this->redis->get('foo'), 'bar-20.5');
++
+ /* Invalid third arguments */
+- $this->assertFalse($this->redis->set('foo','bar','baz'));
+- $this->assertFalse($this->redis->set('foo','bar',new StdClass()));
++ $this->assertFalse(@$this->redis->set('foo','bar','baz'));
++ $this->assertFalse(@$this->redis->set('foo','bar',new StdClass()));
+
+ /* Set if not exist */
+ $this->redis->del('foo');
+@@ -6064,6 +6069,21 @@ class Redis_Test extends TestSuite
+ }
+ }
+
++ /* Regression test for issue-1831 (XINFO STREAM on an empty stream) */
++ public function testXInfoEmptyStream() {
++ /* Configure an empty stream */
++ $this->redis->del('s');
++ $this->redis->xAdd('s', '*', ['foo' => 'bar']);
++ $this->redis->xTrim('s', 0);
++
++ $arr_info = $this->redis->xInfo('STREAM', 's');
++
++ $this->assertTrue(is_array($arr_info));
++ $this->assertEquals(0, $arr_info['length']);
++ $this->assertEquals(NULL, $arr_info['first-entry']);
++ $this->assertEquals(NULL, $arr_info['last-entry']);
++ }
++
+ public function testInvalidAuthArgs() {
+ $obj_new = $this->newInstance();
+
+@@ -6082,9 +6102,15 @@ class Redis_Test extends TestSuite
+
+ foreach ($arr_args as $arr_arg) {
+ try {
+- @call_user_func_array([$obj_new, 'auth'], $arr_arg);
++ if (is_array($arr_arg)) {
++ @call_user_func_array([$obj_new, 'auth'], $arr_arg);
++ } else {
++ call_user_func([$obj_new, 'auth']);
++ }
+ } catch (Exception $ex) {
+ unset($ex); /* Suppress intellisense warning */
++ } catch (ArgumentCountError $ex) {
++ unset($ex); /* Suppress intellisense warning */
+ }
+ }
+ }
+diff --git a/tests/startSession.php b/tests/startSession.php
+index c0ae188..f82c17e 100644
+--- a/tests/startSession.php
++++ b/tests/startSession.php
+@@ -22,11 +22,11 @@ ini_set('redis.session.lock_retries', $lock_retries);
+ ini_set('redis.session.lock_expire', $lock_expire);
+ ini_set('session.gc_maxlifetime', $sessionLifetime);
+
+-if (isset($argv[8])) {
++if (isset($argv[10])) {
+ ini_set('redis.session.locking_enabled', $argv[10]);
+ }
+
+-if (isset($argv[9])) {
++if (isset($argv[11])) {
+ ini_set('redis.session.lock_wait_time', $argv[11]);
+ }
+