Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Improve RedisCluster slot caching and invalidation. #2655

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
Loading
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add invalidateSlotCaches() function to invalidate any slot caches.
This commit adds a mechanism to invalidate every `RedisCluster` slot
cache across all forked workers with one call to any of the workers.

The invalidation itslef works by using a `uint64_t` generation counter
which is allocated in shared memory on `MINIT` which is stored in every
slot cache when it is created.

When a user wants to invalidate any of the slot caches they can call
`RedisCluster::invalidateSlotCaches()` which simply does an atomic
increment on this global genertion value.

When a slot cache is retreived from the persistent list, the internal
generation is compared to the global shared memory generation and if
they don't match, the cache is not used, which is the same as
invalidting it as RedisCluster will write a new slot cache on object
destruction.

The feature is only compiled if `config.m4` determines that using `mmap`
to allocate shared memory is available and works.

Additionally, the feature must be explicitly enabled by setting the
`redis.clusters.shared_slot_cache_invalidation`.
  • Loading branch information
michael-grunder committed May 1, 2025
commit fead6c7f27ab2426bbf72951fa2155e829d3af3a
58 changes: 53 additions & 5 deletions 58 cluster_library.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
#include "crc16.h"
#include <zend_exceptions.h>

#ifdef HAVE_REDIS_ATOMICS_MMAP
#include <stdatomic.h>
#include <sys/mman.h>

static _Atomic uint64_t *g_cluster_cache_gen;
static pid_t g_cluster_cache_pid;
#endif

extern zend_class_entry *redis_cluster_exception_ce;
int le_cluster_slot_cache;

Expand Down Expand Up @@ -883,7 +891,7 @@ cluster_free(redisCluster *c, int free_ctx)
if (free_ctx) efree(c);
}

static zend_long cluster_slot_cache_expiry(void) {
static zend_long cluster_cache_expiry(void) {
zend_long expiry;

expiry = INI_INT("redis.clusters.slot_cache_expiry");
Expand All @@ -893,6 +901,38 @@ static zend_long cluster_slot_cache_expiry(void) {
return time(NULL) + expiry;
michael-grunder marked this conversation as resolved.
Show resolved Hide resolved
}

#ifdef HAVE_REDIS_ATOMICS_MMAP
void cluster_cache_gen_init(void) {
g_cluster_cache_pid = getpid();
g_cluster_cache_gen = mmap(NULL, sizeof(uint64_t), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
}

void cluster_cache_gen_free(void) {
if (g_cluster_cache_gen && g_cluster_cache_pid == getpid()) {
munmap(g_cluster_cache_gen, sizeof(uint64_t));
g_cluster_cache_gen = NULL;
}
}

int cluster_cache_gen_invalidate(void) {
if (g_cluster_cache_gen == NULL)
return FAILURE;

atomic_fetch_add_explicit(g_cluster_cache_gen, 1, memory_order_relaxed);

return SUCCESS;
}

static uint64_t cluster_cache_gen(void) {
if (g_cluster_cache_gen == NULL)
return 0;

return atomic_load(g_cluster_cache_gen);
}

#endif

/* Create a cluster slot cache structure */
PHP_REDIS_API
redisCachedCluster *cluster_cache_create(zend_string *hash, HashTable *nodes) {
Expand All @@ -902,7 +942,10 @@ redisCachedCluster *cluster_cache_create(zend_string *hash, HashTable *nodes) {

cc = pecalloc(1, sizeof(*cc), 1);
cc->hash = zend_string_dup(hash, 1);
cc->expiry = cluster_slot_cache_expiry();
cc->expiry = cluster_cache_expiry();
#ifdef HAVE_REDIS_ATOMICS_MMAP
cc->generation = cluster_cache_gen();
#endif

/* Copy nodes */
cc->master = pecalloc(zend_hash_num_elements(nodes), sizeof(*cc->master), 1);
Expand Down Expand Up @@ -3119,7 +3162,6 @@ PHP_REDIS_API redisCachedCluster *cluster_cache_load(zend_string *hash) {

/* Look for cached slot information */
le = zend_hash_find_ptr(&EG(persistent_list), hash);

if (le == NULL)
return NULL;

Expand All @@ -3129,9 +3171,15 @@ PHP_REDIS_API redisCachedCluster *cluster_cache_load(zend_string *hash) {
}

cc = le->ptr;
if (cc->expiry != 0 && cc->expiry <= time(NULL)) {
/* Short circuit if it should be expired */
if (cc->expiry != 0 && cc->expiry <= time(NULL))
return NULL;
michael-grunder marked this conversation as resolved.
Show resolved Hide resolved
}

#ifdef HAVE_REDIS_ATOMICS_MMAP
/* Short circuit if it has been globally invalidated */
if (cluster_cache_gen() != cc->generation)
return NULL;
#endif

return cc;
}
Expand Down
15 changes: 14 additions & 1 deletion 15 cluster_library.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
#include "TSRM.h"
#endif

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

/* Redis cluster hash slots and N-1 which we'll use to find it */
#define REDIS_CLUSTER_SLOTS 16384
#define REDIS_CLUSTER_MOD (REDIS_CLUSTER_SLOTS-1)
Expand Down Expand Up @@ -157,11 +161,13 @@ typedef struct redisCachedMaster {
} redisCachedMaster;

typedef struct redisCachedCluster {
// int rsrc_id; /* Zend resource ID */
zend_string *hash; /* What we're cached by */
redisCachedMaster *master; /* Array of masters */
size_t count; /* Number of masters */
uint64_t expiry; /* Expiry time (if any) */
#ifdef HAVE_REDIS_ATOMICS_MMAP
uint64_t generation; /* Shared invalidation generation */
#endif
} redisCachedCluster;

/* A Redis Cluster master node */
Expand Down Expand Up @@ -387,6 +393,13 @@ PHP_REDIS_API redisCachedCluster *cluster_cache_create(zend_string *hash, HashTa
PHP_REDIS_API void cluster_cache_free(redisCachedCluster *rcc);
PHP_REDIS_API void cluster_init_cache(redisCluster *c, redisCachedCluster *rcc);

/* Conditionally compiled shared slot cache invalidation functions */
#ifdef HAVE_REDIS_ATOMICS_MMAP
void cluster_cache_gen_init(void);
void cluster_cache_gen_free(void);
int cluster_cache_gen_invalidate(void);
#endif

/* Functions to facilitate cluster slot caching */

PHP_REDIS_API char **cluster_sock_read_multibulk_reply(RedisSock *redis_sock, int *len);
Expand Down
19 changes: 19 additions & 0 deletions 19 config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,25 @@ if test "$PHP_REDIS" != "no"; then
fi
fi

dnl Check if we can use C11 atomics and anonymous shared mmap
AC_LINK_IFELSE(
[AC_LANG_PROGRAM([[
#include <stddef.h>
#include <stdatomic.h>
#include <sys/mman.h>
]], [[
static _Atomic int test = 0;
void *ptr = mmap(NULL, 8, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (ptr == (void *)-1) return 1;
atomic_fetch_add(&test, 1);
return 0;
]])],
[AC_DEFINE([HAVE_REDIS_ATOMICS_MMAP], [1],
[Define if C11 atomics and MAP_SHARED|MAP_ANONYMOUS mmap are usable])],
[]
)

AC_CHECK_PROG([GIT], [git], [yes], [no])
if test "$GIT" = "yes" && test -d "$srcdir/.git"; then
AC_DEFINE_UNQUOTED(GIT_REVISION, ["$(git log -1 --format=%H)"], [ ])
Expand Down
20 changes: 19 additions & 1 deletion 20 redis.c
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ PHP_INI_BEGIN()
/* redis cluster */
PHP_INI_ENTRY("redis.clusters.cache_slots", "0", PHP_INI_ALL, NULL)
PHP_INI_ENTRY("redis.clusters.slot_cache_expiry", "0", PHP_INI_ALL, NULL)

#ifdef HAVE_REDIS_ATOMICS_MMAP
PHP_INI_ENTRY("redis.clusters.shared_slot_cache_invalidation", "0",
PHP_INI_ALL, NULL)
#endif
PHP_INI_ENTRY("redis.clusters.auth", "", PHP_INI_ALL, NULL)
PHP_INI_ENTRY("redis.clusters.persistent", "0", PHP_INI_ALL, NULL)
PHP_INI_ENTRY("redis.clusters.read_timeout", "0", PHP_INI_ALL, NULL)
Expand Down Expand Up @@ -145,7 +150,7 @@ zend_module_entry redis_module_entry = {
"redis",
NULL,
PHP_MINIT(redis),
NULL,
PHP_MSHUTDOWN(redis),
NULL,
NULL,
PHP_MINFO(redis),
Expand Down Expand Up @@ -380,6 +385,11 @@ PHP_MINIT_FUNCTION(redis)
"Redis cluster slot cache",
module_number);

#ifdef HAVE_REDIS_ATOMICS_MMAP
/* Initialize shared slot cache invalidation */
cluster_cache_gen_init();
#endif

/* RedisException class */
redis_exception_ce = register_class_RedisException(spl_ce_RuntimeException);

Expand All @@ -395,6 +405,14 @@ PHP_MINIT_FUNCTION(redis)
return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(redis) {
#ifdef HAVE_REDIS_ATOMICS_MMAP
cluster_cache_gen_free();
#endif

return SUCCESS;
}

static const char *
get_available_serializers(void)
{
Expand Down
15 changes: 14 additions & 1 deletion 15 redis_cluster.c
Original file line number Diff line number Diff line change
Expand Up @@ -1770,12 +1770,25 @@ static void redisClearNodeBytes(redisClusterNode *node) {

PHP_METHOD(RedisCluster, flushSlotCache) {
redisCluster *c = GET_CONTEXT();

ZEND_PARSE_PARAMETERS_NONE();

RETURN_BOOL(cluster_cache_clear(c) == SUCCESS);
}

#ifdef HAVE_REDIS_ATOMICS_MMAP
PHP_METHOD(RedisCluster, invalidateSlotCaches) {
ZEND_PARSE_PARAMETERS_NONE();

if (INI_INT("redis.clusters.shared_slot_cache_invalidation") == 0) {
php_error_docref(NULL, E_WARNING, "Shared slot cache invalidation disabled");
RETURN_FALSE;
}

RETURN_BOOL(cluster_cache_gen_invalidate() == SUCCESS);
}
#endif

PHP_METHOD(RedisCluster, gettransferredbytes) {
redisCluster *c = GET_CONTEXT();
zend_long rx = 0, tx = 0;
Expand Down
10 changes: 10 additions & 0 deletions 10 redis_cluster.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ public function _redir(): string|null;
*/
public function flushSlotCache(): bool;

#ifdef HAVE_REDIS_ATOMICS_MMAP
/**
* Invaalidate all slot caches for across all workers. Only available on
* linux like systems with c11 atomics and shared memory allocation
*
* @return bool Whether we could invalidate any cache(es)
*/
public static function invalidateSlotCaches(): bool;
#endif

/**
* @see Redis::acl
*/
Expand Down
32 changes: 21 additions & 11 deletions 32 redis_cluster_arginfo.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: db05ec768efb80715ac68fe0a1ab65d37de1d390 */
* Stub hash: b1d3eb09f86ccffaa4cf0cc6885550e7d4b6da04 */

ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster___construct, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 1)
Expand Down Expand Up @@ -44,6 +44,11 @@ ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_RedisCluster_flushSlotCache, 0, 0, _IS_BOOL, 0)
ZEND_END_ARG_INFO()

#if defined(HAVE_REDIS_ATOMICS_MMAP)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_RedisCluster_invalidateSlotCaches, 0, 0, _IS_BOOL, 0)
ZEND_END_ARG_INFO()
#endif

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_RedisCluster_acl, 0, 2, IS_MIXED, 0)
ZEND_ARG_TYPE_MASK(0, key_or_address, MAY_BE_STRING|MAY_BE_ARRAY, NULL)
ZEND_ARG_TYPE_INFO(0, subcmd, IS_STRING, 0)
Expand Down Expand Up @@ -1064,7 +1069,6 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_RedisCluster_zdiff, 0,
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null")
ZEND_END_ARG_INFO()


ZEND_METHOD(RedisCluster, __construct);
ZEND_METHOD(RedisCluster, _compress);
ZEND_METHOD(RedisCluster, _uncompress);
Expand All @@ -1076,6 +1080,9 @@ ZEND_METHOD(RedisCluster, _prefix);
ZEND_METHOD(RedisCluster, _masters);
ZEND_METHOD(RedisCluster, _redir);
ZEND_METHOD(RedisCluster, flushSlotCache);
#if defined(HAVE_REDIS_ATOMICS_MMAP)
ZEND_METHOD(RedisCluster, invalidateSlotCaches);
#endif
ZEND_METHOD(RedisCluster, acl);
ZEND_METHOD(RedisCluster, append);
ZEND_METHOD(RedisCluster, bgrewriteaof);
Expand Down Expand Up @@ -1295,7 +1302,6 @@ ZEND_METHOD(RedisCluster, zdiffstore);
ZEND_METHOD(RedisCluster, zunion);
ZEND_METHOD(RedisCluster, zdiff);


static const zend_function_entry class_RedisCluster_methods[] = {
ZEND_ME(RedisCluster, __construct, arginfo_class_RedisCluster___construct, ZEND_ACC_PUBLIC)
ZEND_ME(RedisCluster, _compress, arginfo_class_RedisCluster__compress, ZEND_ACC_PUBLIC)
Expand All @@ -1308,6 +1314,9 @@ static const zend_function_entry class_RedisCluster_methods[] = {
ZEND_ME(RedisCluster, _masters, arginfo_class_RedisCluster__masters, ZEND_ACC_PUBLIC)
ZEND_ME(RedisCluster, _redir, arginfo_class_RedisCluster__redir, ZEND_ACC_PUBLIC)
ZEND_ME(RedisCluster, flushSlotCache, arginfo_class_RedisCluster_flushSlotCache, ZEND_ACC_PUBLIC)
#if defined(HAVE_REDIS_ATOMICS_MMAP)
ZEND_ME(RedisCluster, invalidateSlotCaches, arginfo_class_RedisCluster_invalidateSlotCaches, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
#endif
ZEND_ME(RedisCluster, acl, arginfo_class_RedisCluster_acl, ZEND_ACC_PUBLIC)
ZEND_ME(RedisCluster, append, arginfo_class_RedisCluster_append, ZEND_ACC_PUBLIC)
ZEND_ME(RedisCluster, bgrewriteaof, arginfo_class_RedisCluster_bgrewriteaof, ZEND_ACC_PUBLIC)
Expand Down Expand Up @@ -1529,17 +1538,16 @@ static const zend_function_entry class_RedisCluster_methods[] = {
ZEND_FE_END
};


static const zend_function_entry class_RedisClusterException_methods[] = {
ZEND_FE_END
};

static zend_class_entry *register_class_RedisCluster(void)
{
zend_class_entry ce, *class_entry;

INIT_CLASS_ENTRY(ce, "RedisCluster", class_RedisCluster_methods);
#if (PHP_VERSION_ID >= 80400)
class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0);
#else
class_entry = zend_register_internal_class_ex(&ce, NULL);
#endif

zval const_OPT_SLAVE_FAILOVER_value;
ZVAL_LONG(&const_OPT_SLAVE_FAILOVER_value, REDIS_OPT_FAILOVER);
Expand Down Expand Up @@ -1570,13 +1578,11 @@ static zend_class_entry *register_class_RedisCluster(void)
zend_string *const_FAILOVER_DISTRIBUTE_SLAVES_name = zend_string_init_interned("FAILOVER_DISTRIBUTE_SLAVES", sizeof("FAILOVER_DISTRIBUTE_SLAVES") - 1, 1);
zend_declare_class_constant_ex(class_entry, const_FAILOVER_DISTRIBUTE_SLAVES_name, &const_FAILOVER_DISTRIBUTE_SLAVES_value, ZEND_ACC_PUBLIC, NULL);
zend_string_release(const_FAILOVER_DISTRIBUTE_SLAVES_name);
#if (PHP_VERSION_ID >= 80000)


zend_string *attribute_name_SensitiveParameter_func___construct_arg5_0 = zend_string_init_interned("SensitiveParameter", sizeof("SensitiveParameter") - 1, 1);
zend_add_parameter_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "__construct", sizeof("__construct") - 1), 5, attribute_name_SensitiveParameter_func___construct_arg5_0, 0);
zend_string_release(attribute_name_SensitiveParameter_func___construct_arg5_0);
#endif

return class_entry;
}
Expand All @@ -1585,8 +1591,12 @@ static zend_class_entry *register_class_RedisClusterException(zend_class_entry *
{
zend_class_entry ce, *class_entry;

INIT_CLASS_ENTRY(ce, "RedisClusterException", class_RedisClusterException_methods);
INIT_CLASS_ENTRY(ce, "RedisClusterException", NULL);
#if (PHP_VERSION_ID >= 80400)
class_entry = zend_register_internal_class_with_flags(&ce, class_entry_RuntimeException, 0);
#else
class_entry = zend_register_internal_class_ex(&ce, class_entry_RuntimeException);
#endif

return class_entry;
}
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.