Sitelet https://github.com/phpredis/phpredis/pull/2753/files
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,21 @@ $obj_cluster = new RedisCluster(NULL, ["host:7000", "host:7001"], 1.5, 1.5, true
// If value is array (even empty), it will connect via TLS. If not, it will connect without TLS.
// Note: If the seeds start with "ssl:// or tls://", it will connect to the seeds via TLS, but the subsequent connections will connect without TLS if this value is null. So, if your nodes require TLS, this value must be an array, even if empty.
$obj_cluster = new RedisCluster(NULL, ["host:7000", "host:7001"], 1.5, 1.5, true, NULL, ["verify_peer" => false]);

// Defer initial CLUSTER SLOTS mapping until first command with $lazy_connect argument set to true
$obj_cluster = new RedisCluster(NULL, ["host:7000", "host:7001"], 1.5, 1.5, true, NULL, null, true);
```

#### Loading a cluster configuration by name
In order to load a named array, one must first define the seed nodes in redis.ini. The following lines would define the cluster 'mycluster', and be loaded automatically by phpredis.
In order to load a named array, one must first define the seed nodes in redis.ini. The following lines would define the cluster 'mycluster', and be lazy-loaded automatically by phpredis.

```ini
# In redis.ini
redis.clusters.seeds = "mycluster[]=localhost:7000&test[]=localhost:7001"
redis.clusters.timeout = "mycluster=5"
redis.clusters.read_timeout = "mycluster=10"
redis.clusters.auth = "mycluster=password"
redis.clusters.lazyconnect = "mycluster=1"
```

Then, this cluster can be loaded by doing the following
Expand All @@ -50,6 +54,8 @@ $obj_cluster = new RedisCluster('mycluster');

On construction, the RedisCluster class will iterate over the provided seed nodes until it can attain a connection to the cluster and run CLUSTER SLOTS to map every node in the cluster locally. Once the keyspace is mapped, RedisCluster will only connect to nodes when it needs to (e.g. you're getting a key that we believe is on that node.)

If `lazy_connect` is enabled, the initial keyspace mapping is deferred until the first command is issued. This can reduce startup overhead when many cluster objects are created but not immediately used. If slot caching is enabled (see below), lazy connect will also skip loading from cache at construction time and will instead map or load the cache upon first use.

## Slot caching
Each time the `RedisCluster` class is constructed from scratch, phpredis needs to execute a `CLUSTER SLOTS` command to map the keyspace. Although this isn't an expensive command, it does require a round trip for each newly created object, which is inefficient. Starting from PhpRedis 5.0.0 these slots can be cached by setting `redis.clusters.cache_slots = 1` in `php.ini`.

Expand Down
18 changes: 15 additions & 3 deletions cluster_library.c
Original file line number Diff line number Diff line change
Expand Up @@ -1556,11 +1556,23 @@ PHP_REDIS_API short cluster_send_command(redisCluster *c, short slot, const char
{
int resp, timedout = 0;
long msstart;
redisCachedCluster *cc;

if (!SLOT(c, slot)) {
zend_throw_exception_ex(redis_cluster_exception_ce, 0,
"The slot %d is not covered by any node in this cluster", slot);
return -1;
if (c->lazy_connect) {
if (CLUSTER_CACHING_ENABLED() && (cc = cluster_cache_load(c->cache_key))) {
cluster_init_cache(c, cc);
}

if (!SLOT(c, slot) && cluster_map_keyspace(c) == FAILURE) {
return -1;
}
}
if (!SLOT(c, slot)) {
zend_throw_exception_ex(redis_cluster_exception_ce, 0,
"The slot %d is not covered by any node in this cluster", slot);
return -1;
}
}
/* Set the slot we're operating against as well as it's socket. These can
* change during our request loop if we have a master failure and are
Expand Down
3 changes: 3 additions & 0 deletions cluster_library.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ typedef struct redisCluster {

/* Zend object handler */
zend_object std;

/* If true, defer initial keyspace mapping until first command */
short lazy_connect;
} redisCluster;

/* RedisCluster response processing callback */
Expand Down
33 changes: 23 additions & 10 deletions redis_cluster.c
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,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, zval *context)
zend_string *pass, zval *context, int lazy_connect)
{
zend_string *hash = NULL, **seeds;
redisCachedCluster *cc;
Expand All @@ -254,20 +254,24 @@ static void redis_cluster_init(redisCluster *c, HashTable *ht_seeds, double time
c->flags->read_timeout = read_timeout;
c->flags->persistent = persistent;
c->waitms = (long)(1000 * (timeout + read_timeout));
c->lazy_connect = lazy_connect ? 1 : 0;

/* Attempt to load slots from cache if caching is enabled */
if (CLUSTER_CACHING_ENABLED()) {
/* Exit early if we can load from cache */
hash = cluster_hash_seeds(seeds, nseeds);
if ((cc = cluster_cache_load(hash))) {

if (c->lazy_connect) {
c->cache_key = zend_string_copy(hash);
} else if ((cc = cluster_cache_load(hash))) {
/* Exit early if we can load from cache */
cluster_init_cache(c, cc);
goto cleanup;
}
}

/* Initialize seeds and attempt to map keyspace */
/* Initialize seeds and attempt to map keyspace unless lazy */
cluster_init_seeds(c, seeds, nseeds);
if (cluster_map_keyspace(c) == SUCCESS && hash)
if (!c->lazy_connect && cluster_map_keyspace(c) == SUCCESS && hash)
cluster_cache_store(hash, c->nodes);

cleanup:
Expand All @@ -282,6 +286,7 @@ void redis_cluster_load(redisCluster *c, char *name, int name_len) {
zend_string *user = NULL, *pass = NULL;
double timeout = 0, read_timeout = 0;
int persistent = 0;
int lazy_connect = 0;
char *iptr;
HashTable *ht_seeds = NULL;

Expand Down Expand Up @@ -329,8 +334,16 @@ void redis_cluster_load(redisCluster *c, char *name, int name_len) {
zval_dtor(&z_tmp);
}

/* Lazy connect */
if ((iptr = INI_STR("redis.clusters.lazyconnect")) != NULL) {
array_init(&z_tmp);
sapi_module.treat_data(PARSE_STRING, estrdup(iptr), &z_tmp);
redis_conf_bool(Z_ARRVAL(z_tmp), name, name_len, &lazy_connect);
zval_dtor(&z_tmp);
}

/* Attempt to create/connect to the cluster */
redis_cluster_init(c, ht_seeds, timeout, read_timeout, persistent, user, pass, NULL);
redis_cluster_init(c, ht_seeds, timeout, read_timeout, persistent, user, pass, NULL, lazy_connect);

/* Clean up */
zval_dtor(&z_seeds);
Expand All @@ -348,15 +361,15 @@ PHP_METHOD(RedisCluster, __construct) {
zend_string *user = NULL, *pass = NULL;
double timeout = 0.0, read_timeout = 0.0;
size_t name_len;
zend_bool persistent = 0;
zend_bool persistent = 0, lazy_connect = 0;
redisCluster *c = GET_CONTEXT();
char *name;

// Parse arguments
if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(),
"Os!|addbza!", &object, redis_cluster_ce, &name,
"Os!|addbza!b", &object, redis_cluster_ce, &name,
&name_len, &z_seeds, &timeout, &read_timeout,
&persistent, &z_auth, &context) == FAILURE)
&persistent, &z_auth, &context, &lazy_connect) == FAILURE)
{
RETURN_FALSE;
}
Expand All @@ -373,7 +386,7 @@ PHP_METHOD(RedisCluster, __construct) {
/* The normal case, loading from arguments */
redis_extract_auth_info(z_auth, &user, &pass);
redis_cluster_init(c, Z_ARRVAL_P(z_seeds), timeout, read_timeout,
persistent, user, pass, context);
persistent, user, pass, context, lazy_connect);

if (user) zend_string_release(user);
if (pass) zend_string_release(pass);
Expand Down
2 changes: 1 addition & 1 deletion redis_cluster.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class RedisCluster {
*/
public const FAILOVER_DISTRIBUTE_SLAVES = UNKNOWN;

public function __construct(string|null $name, ?array $seeds = null, int|float $timeout = 0, int|float $read_timeout = 0, bool $persistent = false, #[\SensitiveParameter] mixed $auth = null, ?array $context = null);
public function __construct(string|null $name, ?array $seeds = null, int|float $timeout = 0, int|float $read_timeout = 0, bool $persistent = false, #[\SensitiveParameter] mixed $auth = null, ?array $context = null, bool $lazy_connect = false);

/**
* @see Redis::_compress()
Expand Down
1 change: 1 addition & 0 deletions redis_cluster_arginfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster___construct, 0, 0, 1)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, persistent, _IS_BOOL, 0, "false")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, auth, IS_MIXED, 0, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 1, "null")
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, lazy_connect, _IS_BOOL, 0, "false")
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_RedisCluster__compress, 0, 1, IS_STRING, 0)
Expand Down
5 changes: 5 additions & 0 deletions tests/RedisClusterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,11 @@ public function testNullArray() {
$this->redis->setOption(Redis::OPT_NULL_MULTIBULK_AS_NULL, false);
}

public function testLazyConnectConstruct() {
$rc = new RedisCluster(NULL, self::$seeds, 1, 1, true, $this->getAuth(), null, true);
$this->assertTrue($rc->ping('lazy'));
}

protected function execWaitAOF() {
return $this->redis->waitaof(uniqid(), 0, 0, 0);
}
Expand Down
Loading