From 7278a0c26affb140b72b1465446119d7740a6b24 Mon Sep 17 00:00:00 2001 From: "debing.sun" Date: Sun, 7 Jan 2024 18:10:29 +0800 Subject: [PATCH 01/13] Make RM_Yield thread-safe (#12905) ## Issues and solutions from #12817 1. Touch ProcessingEventsWhileBlocked and calling moduleCount() without GIL in afterSleep() - Introduced: Version: 7.0.0 PR: #9963 - Harm Level: Very High If the module thread calls `RM_Yield()` before the main thread enters afterSleep(), and modifies `ProcessingEventsWhileBlocked`(+1), it will cause the main thread to not wait for GIL, which can lead to all kinds of unforeseen problems, including memory data corruption. - Initial / Abandoned Solution: * Added `__thread` specifier for ProcessingEventsWhileBlocked. `ProcessingEventsWhileBlocked` is used to protect against nested event processing, but event processing in the main thread and module threads should be completely independent and unaffected, so it is safer to use TLS. * Adding a cached module count to keep track of the current number of modules, to avoid having to use `dictSize()`. - Related Warnings: ``` WARNING: ThreadSanitizer: data race (pid=1136) Write of size 4 at 0x0001045990c0 by thread T4 (mutexes: write M0): #0 processEventsWhileBlocked networking.c:4135 (redis-server:arm64+0x10006d124) #1 RM_Yield module.c:2410 (redis-server:arm64+0x10018b66c) #2 bg_call_worker :83232836 (blockedclient.so:arm64+0x16a8) Previous read of size 4 at 0x0001045990c0 by main thread: #0 afterSleep server.c:1861 (redis-server:arm64+0x100024f98) #1 aeProcessEvents ae.c:408 (redis-server:arm64+0x10000fd64) #2 aeMain ae.c:496 (redis-server:arm64+0x100010f0c) #3 main server.c:7220 (redis-server:arm64+0x10003f38c) ``` 2. aeApiPoll() is not thread-safe When using RM_Yield to handle events in a module thread, if the main thread has not yet entered `afterSleep()`, both the module thread and the main thread may touch `server.el` at the same time. - Introduced: Version: 7.0.0 PR: #9963 - Old / Abandoned Solution: Adding a new mutex to protect timing between after beforeSleep() and before afterSleep(). Defect: If the main thread enters the ae loop without any IO events, it will wait until the next timeout or until there is any event again, and the module thread will always hang until the main thread leaves the event loop. - Related Warnings: ``` SUMMARY: ThreadSanitizer: data race ae_kqueue.c:55 in addEventMask ================== ================== WARNING: ThreadSanitizer: data race (pid=14682) Write of size 4 at 0x000100b54000 by thread T9 (mutexes: write M0): #0 aeApiPoll ae_kqueue.c:175 (redis-server:arm64+0x100010588) #1 aeProcessEvents ae.c:399 (redis-server:arm64+0x10000fb84) #2 processEventsWhileBlocked networking.c:4138 (redis-server:arm64+0x10006d3c4) #3 RM_Yield module.c:2410 (redis-server:arm64+0x10018b66c) #4 bg_call_worker :16042052 (blockedclient.so:arm64+0x169c) Previous write of size 4 at 0x000100b54000 by main thread: #0 aeApiPoll ae_kqueue.c:175 (redis-server:arm64+0x100010588) #1 aeProcessEvents ae.c:399 (redis-server:arm64+0x10000fb84) #2 aeMain ae.c:496 (redis-server:arm64+0x100010da8) #3 main server.c:7238 (redis-server:arm64+0x10003f51c) ``` ## The final fix as the comments: https://github.com/redis/redis/pull/12817#discussion_r1436427232 Optimized solution based on the above comment: First, we add `module_gil_acquring` to indicate whether the main thread is currently in the acquiring GIL state. When the module thread starts to yield, there are two possibilities(we assume the caller keeps the GIL): 1. The main thread is in the mid of beforeSleep() and afterSleep(), that is, `module_gil_acquring` is not 1 now. At this point, the module thread will wake up the main thread through the pipe and leave the yield, waiting for the next yield when the main thread may already in the acquiring GIL state. 2. The main thread is in the acquiring GIL state. The module thread release the GIL, yielding CPU to give the main thread an opportunity to start event processing, and then acquire the GIL again until the main thread releases it. This is what https://github.com/redis/redis/pull/12817#discussion_r1436427232 mentioned direction. --------- Co-authored-by: Oran Agra --- src/module.c | 29 ++++++++++++++++++++++++++++- src/server.c | 2 ++ src/server.h | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/module.c b/src/module.c index 1cb418c9b..2c20d6c25 100644 --- a/src/module.c +++ b/src/module.c @@ -2363,7 +2363,33 @@ void RM_Yield(RedisModuleCtx *ctx, int flags, const char *busy_reply) { server.busy_module_yield_flags |= BUSY_MODULE_YIELD_CLIENTS; /* Let redis process events */ - processEventsWhileBlocked(); + if (!pthread_equal(server.main_thread_id, pthread_self())) { + /* If we are not in the main thread, we defer event loop processing to the main thread + * after the main thread enters acquiring GIL state in order to protect the event + * loop (ae.c) and avoid potential race conditions. */ + + int acquiring; + atomicGet(server.module_gil_acquring, acquiring); + if (!acquiring) { + /* If the main thread has not yet entered the acquiring GIL state, + * we attempt to wake it up and exit without waiting for it to + * acquire the GIL. This avoids blocking the caller, allowing them to + * continue with unfinished tasks before the next yield. + * We assume the caller keeps the GIL locked. */ + if (write(server.module_pipe[1],"A",1) != 1) { + /* Ignore the error, this is best-effort. */ + } + } else { + /* Release the GIL, yielding CPU to give the main thread an opportunity to start + * event processing, and then acquire the GIL again until the main thread releases it. */ + moduleReleaseGIL(); + sched_yield(); + moduleAcquireGIL(); + } + } else { + /* If we are in the main thread, we can safely process events. */ + processEventsWhileBlocked(); + } server.busy_module_yield_reply = prev_busy_module_yield_reply; /* Possibly restore the previous flags in case of two nested contexts @@ -11849,6 +11875,7 @@ void moduleInitModulesSystem(void) { moduleUnblockedClients = listCreate(); server.loadmodule_queue = listCreate(); server.module_configs_queue = dictCreate(&sdsKeyValueHashDictType); + server.module_gil_acquring = 0; modules = dictCreate(&modulesDictType); moduleAuthCallbacks = listCreate(); diff --git a/src/server.c b/src/server.c index 5c9870f0e..eaad9dc7c 100644 --- a/src/server.c +++ b/src/server.c @@ -1793,7 +1793,9 @@ void afterSleep(struct aeEventLoop *eventLoop) { mstime_t latency; latencyStartMonitor(latency); + atomicSet(server.module_gil_acquring, 1); moduleAcquireGIL(); + atomicSet(server.module_gil_acquring, 0); moduleFireServerEvent(REDISMODULE_EVENT_EVENTLOOP, REDISMODULE_SUBEVENT_EVENTLOOP_AFTER_SLEEP, NULL); diff --git a/src/server.h b/src/server.h index b1fa542fd..f89feebd6 100644 --- a/src/server.h +++ b/src/server.h @@ -1581,6 +1581,7 @@ struct redisServer { int module_pipe[2]; /* Pipe used to awake the event loop by module threads. */ pid_t child_pid; /* PID of current child */ int child_type; /* Type of current child */ + redisAtomic int module_gil_acquring; /* Indicates whether the GIL is being acquiring by the main thread. */ /* Networking */ int port; /* TCP listening port */ int tls_port; /* TLS listening port */ From 30fe74363886beaec51e19a4d17eb1191fd16c93 Mon Sep 17 00:00:00 2001 From: "debing.sun" Date: Fri, 19 Jan 2024 21:12:49 +0800 Subject: [PATCH 02/13] Fix race condition issues between the main thread and module threads (#12817) Fix #12785 and other race condition issues. See the following isolated comments. The following report was obtained using SANITIZER thread. ```sh make SANITIZER=thread ./runtest-moduleapi --config io-threads 4 --config io-threads-do-reads yes --accurate ``` 1. Fixed thread-safe issue in RM_UnblockClient() Related discussion: https://github.com/redis/redis/pull/12817#issuecomment-1831181220 * When blocking a client in a module using `RM_BlockClientOnKeys()` or `RM_BlockClientOnKeysWithFlags()` with a timeout_callback, calling RM_UnblockClient() in module threads can lead to race conditions in `updateStatsOnUnblock()`. - Introduced: Version: 6.2 PR: #7491 - Touch: `server.stat_numcommands`, `cmd->latency_histogram`, `server.slowlog`, and `server.latency_events` - Harm Level: High Potentially corrupts the memory data of `cmd->latency_histogram`, `server.slowlog`, and `server.latency_events` - Solution: Differentiate whether the call to moduleBlockedClientTimedOut() comes from the module or the main thread. Since we can't know if RM_UnblockClient() comes from module threads, we always assume it does and let `updateStatsOnUnblock()` asynchronously update the unblock status. * When error reply is called in timeout_callback(), ctx is not thread-safe, eventually lead to race conditions in `afterErrorReply`. - Introduced: Version: 6.2 PR: #8217 - Touch `server.stat_total_error_replies`, `server.errors`, - Harm Level: High Potentially corrupts the memory data of `server.errors` - Solution: Make the ctx in `timeout_callback()` with `REDISMODULE_CTX_THREAD_SAFE`, and asynchronously reply errors to the client. 2. Made RM_Reply*() family API thread-safe Related discussion: https://github.com/redis/redis/pull/12817#discussion_r1408707239 Call chain: `RM_Reply*()` -> `_addReplyToBufferOrList()` -> touch server.current_client - Introduced: Version: 7.2.0 PR: #12326 - Harm Level: None Since the module fake client won't have the `CLIENT_PUSHING` flag, even if we touch server.current_client, we can still exit after `c->flags & CLIENT_PUSHING`. - Solution Checking `c->flags & CLIENT_PUSHING` earlier. 3. Made freeClient() thread-safe Fix #12785 - Introduced: Version: 4.0 Commit: https://github.com/redis/redis/commit/3fcf959e609e850a114d4016843e4c991066ebac - Harm Level: Moderate * Trigger assertion It happens when the module thread calls freeClient while the io-thread is in progress, which just triggers an assertion, and doesn't make any race condiaions. * Touch `server.current_client`, `server.stat_clients_type_memory`, and `clientMemUsageBucket->clients`. It happens between the main thread and the module threads, may cause data corruption. 1. Error reset `server.current_client` to NULL, but theoretically this won't happen, because the module has already reset `server.current_client` to old value before entering freeClient. 2. corrupts `clientMemUsageBucket->clients` in updateClientMemUsageAndBucket(). 3. Causes server.stat_clients_type_memory memory statistics to be inaccurate. - Solution: * No longer counts memory usage on fake clients, to avoid updating `server.stat_clients_type_memory` in freeClient. * No longer resetting `server.current_client` in unlinkClient, because the fake client won't be evicted or disconnected in the mid of the process. * Judgment assertion `io_threads_op == IO_THREADS_OP_IDLE` only if c is not a fake client. 4. Fixed free client args without GIL Related discussion: https://github.com/redis/redis/pull/12817#discussion_r1408706695 When freeing retained strings in the module thread (refcount decr), or using them in some way (refcount incr), we should do so while holding the GIL, otherwise, they might be simultaneously freed while the main thread is processing the unblock client state. - Introduced: Version: 6.2.0 PR: #8141 - Harm Level: Low Trigger assertion or double free or memory leak. - Solution: Documenting that module API users need to ensure any access to these retained strings is done with the GIL locked 5. Fix adding fake client to server.clients_pending_write It will incorrectly log the memory usage for the fake client. Related discussion: https://github.com/redis/redis/pull/12817#issuecomment-1851899163 - Introduced: Version: 4.0 Commit: https://github.com/redis/redis/commit/9b01b64430fbc1487429144d2e4e72a4a7fd9db2 - Harm Level: None Only result in NOP - Solution: * Don't add fake client into server.clients_pending_write * Add c->conn assertion for updateClientMemUsageAndBucket() and updateClientMemoryUsage() to avoid same issue in the future. So now it will be the responsibility of the caller of both of them to avoid passing in fake client. 6. Fix calling RM_BlockedClientMeasureTimeStart() and RM_BlockedClientMeasureTimeEnd() without GIL - Introduced: Version: 6.2 PR: #7491 - Harm Level: Low Causes inaccuracies in command latency histogram and slow logs, but does not corrupt memory. - Solution: Module API users, if know that non-thread-safe APIs will be used in multi-threading, need to take responsibility for protecting them with their own locks instead of the GIL, as using the GIL is too expensive. ### Other issue 1. RM_Yield is not thread-safe, fixed via #12905. ### Summarize 1. Fix thread-safe issues for `RM_UnblockClient()`, `freeClient()` and `RM_Yield`, potentially preventing memory corruption, data disorder, or assertion. 2. Updated docs and module test to clarify module API users' responsibility for locking non-thread-safe APIs in multi-threading, such as RM_BlockedClientMeasureTimeStart/End(), RM_FreeString(), RM_RetainString(), and RM_HoldString(). ### About backpot to 7.2 1. The implement of (1) is not too satisfying, would like to get more eyes. 2. (2), (3) can be safely for backport 3. (4), (6) just modifying the module tests and updating the documentation, no need for a backpot. 4. (5) is harmless, no need for a backpot. --------- Co-authored-by: Oran Agra --- src/blocked.c | 2 +- src/module.c | 61 +++++++++++++++++++++------- src/networking.c | 21 +++++----- src/server.c | 5 ++- src/server.h | 2 +- tests/modules/blockedclient.c | 15 +++---- tests/modules/blockonbackground.c | 67 +++++++++++++++++++++++-------- tests/modules/usercall.c | 15 +++---- 8 files changed, 130 insertions(+), 58 deletions(-) diff --git a/src/blocked.c b/src/blocked.c index 7b48fcab6..1bce1eaa7 100644 --- a/src/blocked.c +++ b/src/blocked.c @@ -239,7 +239,7 @@ void replyToBlockedClientTimedOut(client *c) { addReplyLongLong(c,server.fsynced_reploff >= c->bstate.reploffset); addReplyLongLong(c,replicationCountAOFAcksByOffset(c->bstate.reploffset)); } else if (c->bstate.btype == BLOCKED_MODULE) { - moduleBlockedClientTimedOut(c); + moduleBlockedClientTimedOut(c, 0); } else { serverPanic("Unknown btype in replyToBlockedClientTimedOut()."); } diff --git a/src/module.c b/src/module.c index 2c20d6c25..b2c7371c2 100644 --- a/src/module.c +++ b/src/module.c @@ -306,7 +306,6 @@ static size_t moduleTempClientMinCount = 0; /* Min client count in pool since * allow thread safe contexts to execute commands at a safe moment. */ static pthread_mutex_t moduleGIL = PTHREAD_MUTEX_INITIALIZER; - /* Function pointer type for keyspace event notification subscriptions from modules. */ typedef int (*RedisModuleNotificationFunc) (RedisModuleCtx *ctx, int type, const char *event, RedisModuleString *key); @@ -2294,7 +2293,10 @@ ustime_t RM_CachedMicroseconds(void) { * Within the same command, you can call multiple times * RM_BlockedClientMeasureTimeStart() and RM_BlockedClientMeasureTimeEnd() * to accumulate independent time intervals to the background duration. - * This method always return REDISMODULE_OK. */ + * This method always return REDISMODULE_OK. + * + * This function is not thread safe, If used in module thread and blocked callback (possibly main thread) + * simultaneously, it's recommended to protect them with lock owned by caller instead of GIL. */ int RM_BlockedClientMeasureTimeStart(RedisModuleBlockedClient *bc) { elapsedStart(&(bc->background_timer)); return REDISMODULE_OK; @@ -2304,7 +2306,10 @@ int RM_BlockedClientMeasureTimeStart(RedisModuleBlockedClient *bc) { * to calculate the elapsed execution time. * On success REDISMODULE_OK is returned. * This method only returns REDISMODULE_ERR if no start time was - * previously defined ( meaning RM_BlockedClientMeasureTimeStart was not called ). */ + * previously defined ( meaning RM_BlockedClientMeasureTimeStart was not called ). + * + * This function is not thread safe, If used in module thread and blocked callback (possibly main thread) + * simultaneously, it's recommended to protect them with lock owned by caller instead of GIL. */ int RM_BlockedClientMeasureTimeEnd(RedisModuleBlockedClient *bc) { // If the counter is 0 then we haven't called RM_BlockedClientMeasureTimeStart if (!bc->background_timer) @@ -2673,7 +2678,10 @@ RedisModuleString *RM_CreateStringFromStreamID(RedisModuleCtx *ctx, const RedisM * pass ctx as NULL when releasing the string (but passing a context will not * create any issue). Strings created with a context should be freed also passing * the context, so if you want to free a string out of context later, make sure - * to create it using a NULL context. */ + * to create it using a NULL context. + * + * This API is not thread safe, access to these retained strings (if they originated + * from a client command arguments) must be done with GIL locked. */ void RM_FreeString(RedisModuleCtx *ctx, RedisModuleString *str) { decrRefCount(str); if (ctx != NULL) autoMemoryFreed(ctx,REDISMODULE_AM_STRING,str); @@ -2710,7 +2718,10 @@ void RM_FreeString(RedisModuleCtx *ctx, RedisModuleString *str) { * * Threaded modules that reference retained strings from other threads *must* * explicitly trim the allocation as soon as the string is retained. Not doing - * so may result with automatic trimming which is not thread safe. */ + * so may result with automatic trimming which is not thread safe. + * + * This API is not thread safe, access to these retained strings (if they originated + * from a client command arguments) must be done with GIL locked. */ void RM_RetainString(RedisModuleCtx *ctx, RedisModuleString *str) { if (ctx == NULL || !autoMemoryFreed(ctx,REDISMODULE_AM_STRING,str)) { /* Increment the string reference counting only if we can't @@ -2752,7 +2763,10 @@ void RM_RetainString(RedisModuleCtx *ctx, RedisModuleString *str) { * * Threaded modules that reference held strings from other threads *must* * explicitly trim the allocation as soon as the string is held. Not doing - * so may result with automatic trimming which is not thread safe. */ + * so may result with automatic trimming which is not thread safe. + * + * This API is not thread safe, access to these retained strings (if they originated + * from a client command arguments) must be done with GIL locked. */ RedisModuleString* RM_HoldString(RedisModuleCtx *ctx, RedisModuleString *str) { if (str->refcount == OBJ_STATIC_REFCOUNT) { return RM_CreateStringFromString(ctx, str); @@ -8185,7 +8199,7 @@ int RM_UnblockClient(RedisModuleBlockedClient *bc, void *privdata) { * argument, but better to be safe than sorry. */ if (bc->timeout_callback == NULL) return REDISMODULE_ERR; if (bc->unblocked) return REDISMODULE_OK; - if (bc->client) moduleBlockedClientTimedOut(bc->client); + if (bc->client) moduleBlockedClientTimedOut(bc->client, 1); } moduleUnblockClientByHandle(bc,privdata); return REDISMODULE_OK; @@ -8284,8 +8298,10 @@ void moduleHandleBlockedClients(void) { * This needs to be out of the reply callback above given that a * module might not define any callback and still do blocking ops. */ - if (c && !clientHasModuleAuthInProgress(c) && !bc->blocked_on_keys) { - updateStatsOnUnblock(c, bc->background_duration, reply_us, server.stat_total_error_replies != prev_error_replies); + if (c && !clientHasModuleAuthInProgress(c)) { + int had_errors = c->deferred_reply_errors ? !!listLength(c->deferred_reply_errors) : + (server.stat_total_error_replies != prev_error_replies); + updateStatsOnUnblock(c, bc->background_duration, reply_us, had_errors); } if (c != NULL) { @@ -8303,7 +8319,7 @@ void moduleHandleBlockedClients(void) { * if there are pending replies here. This is needed since * during a non blocking command the client may receive output. */ if (!clientHasModuleAuthInProgress(c) && clientHasPendingReplies(c) && - !(c->flags & CLIENT_PENDING_WRITE)) + !(c->flags & CLIENT_PENDING_WRITE) && c->conn) { c->flags |= CLIENT_PENDING_WRITE; listLinkNodeHead(server.clients_pending_write, &c->clients_pending_write_node); @@ -8338,8 +8354,15 @@ int moduleBlockedClientMayTimeout(client *c) { /* Called when our client timed out. After this function unblockClient() * is called, and it will invalidate the blocked client. So this function * does not need to do any cleanup. Eventually the module will call the - * API to unblock the client and the memory will be released. */ -void moduleBlockedClientTimedOut(client *c) { + * API to unblock the client and the memory will be released. + * + * If this function is called from a module, we handle the timeout callback + * and the update of the unblock status in a thread-safe manner to avoid race + * conditions with the main thread. + * If this function is called from the main thread, we must handle the unblocking + * of the client synchronously. This ensures that we can reply to the client before + * resetClient() is called. */ +void moduleBlockedClientTimedOut(client *c, int from_module) { RedisModuleBlockedClient *bc = c->bstate.module_blocked_handle; /* Protect against re-processing: don't serve clients that are already @@ -8348,14 +8371,22 @@ void moduleBlockedClientTimedOut(client *c) { if (bc->unblocked) return; RedisModuleCtx ctx; - moduleCreateContext(&ctx, bc->module, REDISMODULE_CTX_BLOCKED_TIMEOUT); + int flags = REDISMODULE_CTX_BLOCKED_TIMEOUT; + if (from_module) flags |= REDISMODULE_CTX_THREAD_SAFE; + moduleCreateContext(&ctx, bc->module, flags); ctx.client = bc->client; ctx.blocked_client = bc; ctx.blocked_privdata = bc->privdata; - long long prev_error_replies = server.stat_total_error_replies; + + long long prev_error_replies; + if (!from_module) + prev_error_replies = server.stat_total_error_replies; + bc->timeout_callback(&ctx,(void**)c->argv,c->argc); moduleFreeContext(&ctx); - updateStatsOnUnblock(c, bc->background_duration, 0, server.stat_total_error_replies != prev_error_replies); + + if (!from_module) + updateStatsOnUnblock(c, bc->background_duration, 0, server.stat_total_error_replies != prev_error_replies); /* For timeout events, we do not want to call the disconnect callback, * because the blocked client will be automatically disconnected in diff --git a/src/networking.c b/src/networking.c index 9674526be..7007141a8 100644 --- a/src/networking.c +++ b/src/networking.c @@ -413,8 +413,9 @@ void _addReplyToBufferOrList(client *c, const char *s, size_t len) { * to a channel which we are subscribed to, then we wanna postpone that message to be added * after the command's reply (specifically important during multi-exec). the exception is * the SUBSCRIBE command family, which (currently) have a push message instead of a proper reply. - * The check for executing_client also avoids affecting push messages that are part of eviction. */ - if (c == server.current_client && (c->flags & CLIENT_PUSHING) && + * The check for executing_client also avoids affecting push messages that are part of eviction. + * Check CLIENT_PUSHING first to avoid race conditions, as it's absent in module's fake client. */ + if ((c->flags & CLIENT_PUSHING) && c == server.current_client && server.executing_client && !cmdHasPushAsReply(server.executing_client->cmd)) { _addReplyProtoToList(c,server.pending_push_messages,s,len); @@ -1433,7 +1434,7 @@ void unlinkClient(client *c) { listNode *ln; /* If this is marked as current client unset it. */ - if (server.current_client == c) server.current_client = NULL; + if (c->conn && server.current_client == c) server.current_client = NULL; /* Certain operations must be done only if the client has an active connection. * If the client was already unlinked or if it's a "fake client" the @@ -1477,7 +1478,7 @@ void unlinkClient(client *c) { } /* Remove from the list of pending reads if needed. */ - serverAssert(io_threads_op == IO_THREADS_OP_IDLE); + serverAssert(!c->conn || io_threads_op == IO_THREADS_OP_IDLE); if (c->pending_read_list_node != NULL) { listDelNode(server.clients_pending_read,c->pending_read_list_node); c->pending_read_list_node = NULL; @@ -1630,6 +1631,12 @@ void freeClient(client *c) { reqresReset(c, 1); #endif + /* Remove the contribution that this client gave to our + * incrementally computed memory usage. */ + if (c->conn) + server.stat_clients_type_memory[c->last_memory_type] -= + c->last_memory_usage; + /* Unlink the client: this will close the socket, remove the I/O * handlers, and remove references of the client from different * places where active clients may be referenced. */ @@ -1678,10 +1685,6 @@ void freeClient(client *c) { * we lost the connection with the master. */ if (c->flags & CLIENT_MASTER) replicationHandleMasterDisconnection(); - /* Remove the contribution that this client gave to our - * incrementally computed memory usage. */ - server.stat_clients_type_memory[c->last_memory_type] -= - c->last_memory_usage; /* Remove client from memory usage buckets */ if (c->mem_usage_bucket) { c->mem_usage_bucket->mem_usage_sum -= c->last_memory_usage; @@ -2470,7 +2473,7 @@ int processCommandAndResetClient(client *c) { commandProcessed(c); /* Update the client's memory to include output buffer growth following the * processed command. */ - updateClientMemUsageAndBucket(c); + if (c->conn) updateClientMemUsageAndBucket(c); } if (server.current_client == NULL) deadclient = 1; diff --git a/src/server.c b/src/server.c index eaad9dc7c..213942441 100644 --- a/src/server.c +++ b/src/server.c @@ -872,6 +872,7 @@ static inline clientMemUsageBucket *getMemUsageBucket(size_t mem) { * usage bucket. */ void updateClientMemoryUsage(client *c) { + serverAssert(c->conn); size_t mem = getClientMemoryUsage(c, NULL); int type = getClientType(c); /* Now that we have the memory used by the client, remove the old @@ -884,7 +885,7 @@ void updateClientMemoryUsage(client *c) { } int clientEvictionAllowed(client *c) { - if (server.maxmemory_clients == 0 || c->flags & CLIENT_NO_EVICT) { + if (server.maxmemory_clients == 0 || c->flags & CLIENT_NO_EVICT || !c->conn) { return 0; } int type = getClientType(c); @@ -924,7 +925,7 @@ void removeClientFromMemUsageBucket(client *c, int allow_eviction) { * returns 1 if client eviction for this client is allowed, 0 otherwise. */ int updateClientMemUsageAndBucket(client *c) { - serverAssert(io_threads_op == IO_THREADS_OP_IDLE); + serverAssert(io_threads_op == IO_THREADS_OP_IDLE && c->conn); int allow_eviction = clientEvictionAllowed(c); removeClientFromMemUsageBucket(c, allow_eviction); diff --git a/src/server.h b/src/server.h index f89feebd6..02d60d5a5 100644 --- a/src/server.h +++ b/src/server.h @@ -2483,7 +2483,7 @@ void moduleFreeContext(struct RedisModuleCtx *ctx); void moduleCallCommandUnblockedHandler(client *c); void unblockClientFromModule(client *c); void moduleHandleBlockedClients(void); -void moduleBlockedClientTimedOut(client *c); +void moduleBlockedClientTimedOut(client *c, int from_module); void modulePipeReadable(aeEventLoop *el, int fd, void *privdata, int mask); size_t moduleCount(void); void moduleAcquireGIL(void); diff --git a/tests/modules/blockedclient.c b/tests/modules/blockedclient.c index 92060fd33..23030cef4 100644 --- a/tests/modules/blockedclient.c +++ b/tests/modules/blockedclient.c @@ -102,6 +102,7 @@ typedef struct { void *bg_call_worker(void *arg) { bg_call_data *bg = arg; + RedisModuleBlockedClient *bc = bg->bc; // Get Redis module context RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bg->bc); @@ -135,6 +136,12 @@ void *bg_call_worker(void *arg) { RedisModuleCallReply *rep = RedisModule_Call(ctx, cmd, format, bg->argv + cmd_pos + 1, bg->argc - cmd_pos - 1); RedisModule_FreeString(NULL, format_redis_str); + /* Free the arguments within GIL to prevent simultaneous freeing in main thread. */ + for (int i=0; iargc; i++) + RedisModule_FreeString(ctx, bg->argv[i]); + RedisModule_Free(bg->argv); + RedisModule_Free(bg); + // Release GIL RedisModule_ThreadSafeContextUnlock(ctx); @@ -147,13 +154,7 @@ void *bg_call_worker(void *arg) { } // Unblock client - RedisModule_UnblockClient(bg->bc, NULL); - - /* Free the arguments */ - for (int i=0; iargc; i++) - RedisModule_FreeString(ctx, bg->argv[i]); - RedisModule_Free(bg->argv); - RedisModule_Free(bg); + RedisModule_UnblockClient(bc, NULL); // Free the Redis module context RedisModule_FreeThreadSafeContext(ctx); diff --git a/tests/modules/blockonbackground.c b/tests/modules/blockonbackground.c index 2e3b1a557..e068e20d9 100644 --- a/tests/modules/blockonbackground.c +++ b/tests/modules/blockonbackground.c @@ -7,12 +7,41 @@ #define UNUSED(x) (void)(x) +typedef struct { + /* Mutex for protecting RedisModule_BlockedClientMeasureTime*() API from race + * conditions due to timeout callback triggered in the main thread. */ + pthread_mutex_t measuretime_mutex; + int measuretime_completed; /* Indicates that time measure has ended and will not continue further */ + int myint; /* Used for replying */ +} BlockPrivdata; + +void blockClientPrivdataInit(RedisModuleBlockedClient *bc) { + BlockPrivdata *block_privdata = RedisModule_Calloc(1, sizeof(*block_privdata)); + block_privdata->measuretime_mutex = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; + RedisModule_BlockClientSetPrivateData(bc, block_privdata); +} + +void blockClientMeasureTimeStart(RedisModuleBlockedClient *bc, BlockPrivdata *block_privdata) { + pthread_mutex_lock(&block_privdata->measuretime_mutex); + RedisModule_BlockedClientMeasureTimeStart(bc); + pthread_mutex_unlock(&block_privdata->measuretime_mutex); +} + +void blockClientMeasureTimeEnd(RedisModuleBlockedClient *bc, BlockPrivdata *block_privdata, int completed) { + pthread_mutex_lock(&block_privdata->measuretime_mutex); + if (!block_privdata->measuretime_completed) { + RedisModule_BlockedClientMeasureTimeEnd(bc); + if (completed) block_privdata->measuretime_completed = 1; + } + pthread_mutex_unlock(&block_privdata->measuretime_mutex); +} + /* Reply callback for blocking command BLOCK.DEBUG */ int HelloBlock_Reply(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { UNUSED(argv); UNUSED(argc); - int *myint = RedisModule_GetBlockedClientPrivateData(ctx); - return RedisModule_ReplyWithLongLong(ctx,*myint); + BlockPrivdata *block_privdata = RedisModule_GetBlockedClientPrivateData(ctx); + return RedisModule_ReplyWithLongLong(ctx,block_privdata->myint); } /* Timeout callback for blocking command BLOCK.DEBUG */ @@ -20,13 +49,16 @@ int HelloBlock_Timeout(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) UNUSED(argv); UNUSED(argc); RedisModuleBlockedClient *bc = RedisModule_GetBlockedClientHandle(ctx); - RedisModule_BlockedClientMeasureTimeEnd(bc); + BlockPrivdata *block_privdata = RedisModule_GetBlockedClientPrivateData(ctx); + blockClientMeasureTimeEnd(bc, block_privdata, 1); return RedisModule_ReplyWithSimpleString(ctx,"Request timedout"); } /* Private data freeing callback for BLOCK.DEBUG command. */ void HelloBlock_FreeData(RedisModuleCtx *ctx, void *privdata) { UNUSED(ctx); + BlockPrivdata *block_privdata = privdata; + pthread_mutex_destroy(&block_privdata->measuretime_mutex); RedisModule_Free(privdata); } @@ -42,19 +74,20 @@ void *BlockDebug_ThreadMain(void *arg) { RedisModuleBlockedClient *bc = targ[0]; long long delay = (unsigned long)targ[1]; long long enable_time_track = (unsigned long)targ[2]; + BlockPrivdata *block_privdata = RedisModule_BlockClientGetPrivateData(bc); + if (enable_time_track) - RedisModule_BlockedClientMeasureTimeStart(bc); + blockClientMeasureTimeStart(bc, block_privdata); RedisModule_Free(targ); struct timespec ts; ts.tv_sec = delay / 1000; ts.tv_nsec = (delay % 1000) * 1000000; nanosleep(&ts, NULL); - int *r = RedisModule_Alloc(sizeof(int)); - *r = rand(); if (enable_time_track) - RedisModule_BlockedClientMeasureTimeEnd(bc); - RedisModule_UnblockClient(bc,r); + blockClientMeasureTimeEnd(bc, block_privdata, 0); + block_privdata->myint = rand(); + RedisModule_UnblockClient(bc,block_privdata); return NULL; } @@ -64,23 +97,22 @@ void *DoubleBlock_ThreadMain(void *arg) { void **targ = arg; RedisModuleBlockedClient *bc = targ[0]; long long delay = (unsigned long)targ[1]; - RedisModule_BlockedClientMeasureTimeStart(bc); + BlockPrivdata *block_privdata = RedisModule_BlockClientGetPrivateData(bc); + blockClientMeasureTimeStart(bc, block_privdata); RedisModule_Free(targ); struct timespec ts; ts.tv_sec = delay / 1000; ts.tv_nsec = (delay % 1000) * 1000000; nanosleep(&ts, NULL); - int *r = RedisModule_Alloc(sizeof(int)); - *r = rand(); - RedisModule_BlockedClientMeasureTimeEnd(bc); + blockClientMeasureTimeEnd(bc, block_privdata, 0); /* call again RedisModule_BlockedClientMeasureTimeStart() and * RedisModule_BlockedClientMeasureTimeEnd and ensure that the * total execution time is 2x the delay. */ - RedisModule_BlockedClientMeasureTimeStart(bc); + blockClientMeasureTimeStart(bc, block_privdata); nanosleep(&ts, NULL); - RedisModule_BlockedClientMeasureTimeEnd(bc); - - RedisModule_UnblockClient(bc,r); + blockClientMeasureTimeEnd(bc, block_privdata, 0); + block_privdata->myint = rand(); + RedisModule_UnblockClient(bc,block_privdata); return NULL; } @@ -107,6 +139,7 @@ int HelloBlock_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int a pthread_t tid; RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,timeout); + blockClientPrivdataInit(bc); /* Here we set a disconnection handler, however since this module will * block in sleep() in a thread, there is not much we can do in the @@ -148,6 +181,7 @@ int HelloBlockNoTracking_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **a pthread_t tid; RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,timeout); + blockClientPrivdataInit(bc); /* Here we set a disconnection handler, however since this module will * block in sleep() in a thread, there is not much we can do in the @@ -184,6 +218,7 @@ int HelloDoubleBlock_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, pthread_t tid; RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,HelloBlock_Reply,HelloBlock_Timeout,HelloBlock_FreeData,0); + blockClientPrivdataInit(bc); /* Now that we setup a blocking client, we need to pass the control * to the thread. However we need to pass arguments to the thread: diff --git a/tests/modules/usercall.c b/tests/modules/usercall.c index 6b23974d4..316de1eea 100644 --- a/tests/modules/usercall.c +++ b/tests/modules/usercall.c @@ -115,6 +115,7 @@ typedef struct { void *bg_call_worker(void *arg) { bg_call_data *bg = arg; + RedisModuleBlockedClient *bc = bg->bc; // Get Redis module context RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bg->bc); @@ -136,6 +137,12 @@ void *bg_call_worker(void *arg) { RedisModuleCallReply *rep = RedisModule_Call(ctx, cmd, format, bg->argv + 3, bg->argc - 3); RedisModule_FreeString(NULL, format_redis_str); + /* Free the arguments within GIL to prevent simultaneous freeing in main thread. */ + for (int i=0; iargc; i++) + RedisModule_FreeString(ctx, bg->argv[i]); + RedisModule_Free(bg->argv); + RedisModule_Free(bg); + // Release GIL RedisModule_ThreadSafeContextUnlock(ctx); @@ -148,13 +155,7 @@ void *bg_call_worker(void *arg) { } // Unblock client - RedisModule_UnblockClient(bg->bc, NULL); - - /* Free the arguments */ - for (int i=0; iargc; i++) - RedisModule_FreeString(ctx, bg->argv[i]); - RedisModule_Free(bg->argv); - RedisModule_Free(bg); + RedisModule_UnblockClient(bc, NULL); // Free the Redis module context RedisModule_FreeThreadSafeContext(ctx); From bdc78175b5e16b968afee2213926e9687216ea41 Mon Sep 17 00:00:00 2001 From: Ping Xie Date: Mon, 4 Mar 2024 17:32:25 -0800 Subject: [PATCH 03/13] Fix PONG message processing for primary-ship tracking during failovers (#13055) This commit updates the processing of PONG gossip messages in the cluster. When a node (B) becomes a replica due to a failover, its PONG messages include its new primary node's (A) information and B's configuration epoch is aligned with A's. This allows observer nodes to identify changes in primary-ship, addressing issues of intermediate states and enhancing cluster state consistency during topology changes. Fix #13018 --- src/cluster.c | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/cluster.c b/src/cluster.c index da0301718..a0944bf23 100644 --- a/src/cluster.c +++ b/src/cluster.c @@ -61,6 +61,7 @@ list *clusterGetNodesInMyShard(clusterNode *node); int clusterNodeAddSlave(clusterNode *master, clusterNode *slave); int clusterAddSlot(clusterNode *n, int slot); int clusterDelSlot(int slot); +int clusterMoveNodeSlots(clusterNode *from_node, clusterNode *to_node); int clusterDelNodeSlots(clusterNode *node); int clusterNodeSetSlotBit(clusterNode *n, int slot); void clusterSetMaster(clusterNode *n); @@ -3058,7 +3059,50 @@ int clusterProcessPacket(clusterLink *link) { if (nodeIsMaster(sender)) { /* Master turned into a slave! Reconfigure the node. */ - clusterDelNodeSlots(sender); + if (master && !memcmp(master->shard_id, sender->shard_id, CLUSTER_NAMELEN)) { + /* `sender` was a primary and was in the same shard as `master`, its new primary */ + if (sender->configEpoch > senderConfigEpoch) { + serverLog(LL_NOTICE, + "Ignore stale message from %.40s (%s) in shard %.40s;" + " gossip config epoch: %llu, current config epoch: %llu", + sender->name, + sender->human_nodename, + sender->shard_id, + (unsigned long long)senderConfigEpoch, + (unsigned long long)sender->configEpoch); + } else { + /* A failover occurred in the shard where `sender` belongs to and `sender` is no longer + * a primary. Update slot assignment to `master`, which is the new primary in the shard */ + int slots = clusterMoveNodeSlots(sender, master); + /* `master` is still a `slave` in this observer node's view; update its role and configEpoch */ + clusterSetNodeAsMaster(master); + master->configEpoch = senderConfigEpoch; + serverLog(LL_NOTICE, "A failover occurred in shard %.40s; node %.40s (%s)" + " lost %d slot(s) to node %.40s (%s) with a config epoch of %llu", + sender->shard_id, + sender->name, + sender->human_nodename, + slots, + master->name, + master->human_nodename, + (unsigned long long) master->configEpoch); + } + } else { + /* `sender` was moved to another shard and has become a replica, remove its slot assignment */ + int slots = clusterDelNodeSlots(sender); + serverLog(LL_NOTICE, "Node %.40s (%s) is no longer master of shard %.40s;" + " removed all %d slot(s) it used to own", + sender->name, + sender->human_nodename, + sender->shard_id, + slots); + if (master != NULL) { + serverLog(LL_NOTICE, "Node %.40s (%s) is now part of shard %.40s", + sender->name, + sender->human_nodename, + master->shard_id); + } + } sender->flags &= ~(CLUSTER_NODE_MASTER| CLUSTER_NODE_MIGRATE_TO); sender->flags |= CLUSTER_NODE_SLAVE; @@ -5010,6 +5054,22 @@ int clusterDelSlot(int slot) { return C_OK; } +/* Transfer slots from `from_node` to `to_node`. + * Iterates over all cluster slots, transferring each slot covered by `from_node` to `to_node`. + * Counts and returns the number of slots transferred. */ +int clusterMoveNodeSlots(clusterNode *from_node, clusterNode *to_node) { + int processed = 0; + + for (int j = 0; j < CLUSTER_SLOTS; j++) { + if (clusterNodeGetSlotBit(from_node, j)) { + clusterDelSlot(j); + clusterAddSlot(to_node, j); + processed++; + } + } + return processed; +} + /* Delete all the slots associated with the specified node. * The number of deleted slots is returned. */ int clusterDelNodeSlots(clusterNode *node) { From 8e72b42a9a3c40c018e5c5fd0a57ee164ee144a4 Mon Sep 17 00:00:00 2001 From: guybe7 Date: Wed, 30 Oct 2024 17:32:51 +0800 Subject: [PATCH 04/13] Modules: defrag CB should take robj, not sds (#13627) Added a log of the keyname in the test modules to reproduce the problem (tests crash without the fix) --- src/defrag.c | 9 ++++++--- tests/modules/defragtest.c | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/defrag.c b/src/defrag.c index 25b4ee45a..1a1ca1bf7 100644 --- a/src/defrag.c +++ b/src/defrag.c @@ -662,8 +662,9 @@ void defragStream(redisDb *db, dictEntry *kde) { void defragModule(redisDb *db, dictEntry *kde) { robj *obj = dictGetVal(kde); serverAssert(obj->type == OBJ_MODULE); - - if (!moduleDefragValue(dictGetKey(kde), obj, db->id)) + robj keyobj; + initStaticStringObject(keyobj, dictGetKey(kde)); + if (!moduleDefragValue(&keyobj, obj, db->id)) defragLater(db, kde); } @@ -806,7 +807,9 @@ int defragLaterItem(dictEntry *de, unsigned long *cursor, long long endtime, int } else if (ob->type == OBJ_STREAM) { return scanLaterStreamListpacks(ob, cursor, endtime); } else if (ob->type == OBJ_MODULE) { - return moduleLateDefrag(dictGetKey(de), ob, cursor, endtime, dbid); + robj keyobj; + initStaticStringObject(keyobj, dictGetKey(de)); + return moduleLateDefrag(&keyobj, ob, cursor, endtime, dbid); } else { *cursor = 0; /* object type may have changed since we schedule it for later */ } diff --git a/tests/modules/defragtest.c b/tests/modules/defragtest.c index 6a02a059f..22f304924 100644 --- a/tests/modules/defragtest.c +++ b/tests/modules/defragtest.c @@ -141,13 +141,14 @@ size_t FragFreeEffort(RedisModuleString *key, const void *value) { } int FragDefrag(RedisModuleDefragCtx *ctx, RedisModuleString *key, void **value) { - REDISMODULE_NOT_USED(key); unsigned long i = 0; int steps = 0; int dbid = RedisModule_GetDbIdFromDefragCtx(ctx); RedisModule_Assert(dbid != -1); + RedisModule_Log(NULL, "notice", "Defrag key: %s", RedisModule_StringPtrLen(key, NULL)); + /* Attempt to get cursor, validate it's what we're exepcting */ if (RedisModule_DefragCursorGet(ctx, &i) == REDISMODULE_OK) { if (i > 0) datatype_resumes++; From 6e819e188b9375447754829bb03b34eb7397fc27 Mon Sep 17 00:00:00 2001 From: nafraf Date: Thu, 21 Nov 2024 01:14:14 -0500 Subject: [PATCH 05/13] Fix module loadex command crash due to invalid config (#13653) Fix to https://github.com/redis/redis/issues/13650 providing an invalid config to a module with datatype crashes when redis tries to unload the module due to the invalid config --------- Co-authored-by: debing.sun --- src/module.c | 13 ++++++++----- src/server.h | 2 +- tests/modules/datatype.c | 9 +++++++++ tests/unit/moduleapi/datatype.tcl | 5 +++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/module.c b/src/module.c index b2c7371c2..f679c0f33 100644 --- a/src/module.c +++ b/src/module.c @@ -12262,7 +12262,7 @@ int moduleLoad(const char *path, void **module_argv, int module_argc, int is_loa } if (post_load_err) { - moduleUnload(ctx.module->name, NULL); + serverAssert(moduleUnload(ctx.module->name, NULL, 1) == C_OK); moduleFreeContext(&ctx); return C_ERR; } @@ -12278,14 +12278,17 @@ int moduleLoad(const char *path, void **module_argv, int module_argc, int is_loa /* Unload the module registered with the specified name. On success * C_OK is returned, otherwise C_ERR is returned and errmsg is set - * with an appropriate message. */ -int moduleUnload(sds name, const char **errmsg) { + * with an appropriate message. + * Only forcefully unload this module, passing forced_unload != 0, + * if it is certain that it has not yet been in use (e.g., immediate + * unload on failed load). */ +int moduleUnload(sds name, const char **errmsg, int forced_unload) { struct RedisModule *module = dictFetchValue(modules,name); if (module == NULL) { *errmsg = "no such module with that name"; return C_ERR; - } else if (listLength(module->types)) { + } else if (listLength(module->types) && !forced_unload) { *errmsg = "the module exports one or more module-side data " "types, can't unload"; return C_ERR; @@ -13078,7 +13081,7 @@ NULL } else if (!strcasecmp(subcmd,"unload") && c->argc == 3) { const char *errmsg = NULL; - if (moduleUnload(c->argv[2]->ptr, &errmsg) == C_OK) + if (moduleUnload(c->argv[2]->ptr, &errmsg, 0) == C_OK) addReply(c,shared.ok); else { if (errmsg == NULL) errmsg = "operation not possible."; diff --git a/src/server.h b/src/server.h index 02d60d5a5..147f730f4 100644 --- a/src/server.h +++ b/src/server.h @@ -2469,7 +2469,7 @@ void moduleInitModulesSystem(void); void moduleInitModulesSystemLast(void); void modulesCron(void); int moduleLoad(const char *path, void **argv, int argc, int is_loadex); -int moduleUnload(sds name, const char **errmsg); +int moduleUnload(sds name, const char **errmsg, int forced_unload); void moduleLoadFromQueue(void); int moduleGetCommandKeysViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); int moduleGetCommandChannelsViaAPI(struct redisCommand *cmd, robj **argv, int argc, getKeysResult *result); diff --git a/tests/modules/datatype.c b/tests/modules/datatype.c index 408d1a526..05cf2337c 100644 --- a/tests/modules/datatype.c +++ b/tests/modules/datatype.c @@ -312,3 +312,12 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) return REDISMODULE_OK; } + +int RedisModule_OnUnload(RedisModuleCtx *ctx) { + REDISMODULE_NOT_USED(ctx); + if (datatype) { + RedisModule_Free(datatype); + datatype = NULL; + } + return REDISMODULE_OK; +} diff --git a/tests/unit/moduleapi/datatype.tcl b/tests/unit/moduleapi/datatype.tcl index 951c060e7..5d1722caa 100644 --- a/tests/unit/moduleapi/datatype.tcl +++ b/tests/unit/moduleapi/datatype.tcl @@ -1,6 +1,11 @@ set testmodule [file normalize tests/modules/datatype.so] start_server {tags {"modules"}} { + test {DataType: test loadex with invalid config} { + catch { r module loadex $testmodule CONFIG invalid_config 1 } e + assert_match {*ERR Error loading the extension*} $e + } + r module load $testmodule test {DataType: Test module is sane, GET/SET work.} { From 9c5c2dc3a8f60ea8e89a7e477a7646ad30f75059 Mon Sep 17 00:00:00 2001 From: Mingyi Kang Date: Sat, 1 Feb 2025 14:09:00 +0800 Subject: [PATCH 06/13] Bump actions/upload-artifact from 3 to 4 (#13780) Update `upload-artifact` from v3 to v4 to avoid the failure of `External Server Tests` (I encountered this error when opening [#13779](https://github.com/redis/redis/pull/13779)): > Error: This request has been automatically failed because it uses a deprecated version of `actions/upload-artifact: v3`. Learn more: https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/ --- .github/workflows/external.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/external.yml b/.github/workflows/external.yml index 3cc03198f..da993f668 100644 --- a/.github/workflows/external.yml +++ b/.github/workflows/external.yml @@ -26,7 +26,7 @@ jobs: --tags -slow - name: Archive redis log if: ${{ failure() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-external-redis-log path: external-redis.log @@ -53,7 +53,7 @@ jobs: --tags -slow - name: Archive redis log if: ${{ failure() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-external-cluster-log path: external-redis.log @@ -76,7 +76,7 @@ jobs: --tags "-slow -needs:debug" - name: Archive redis log if: ${{ failure() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-external-redis-log path: external-redis.log From a26774cee10dcb002d62371e3d01be5e14cff9af Mon Sep 17 00:00:00 2001 From: Benson-li <46148313+li-benson@users.noreply.github.com> Date: Thu, 20 Mar 2025 21:32:12 +0800 Subject: [PATCH 07/13] Fix potential infinite loop of RANDOMKEY during client pause (#13863) The bug mentioned in this [#13862](https://github.com/redis/redis/issues/13862) has been fixed. --------- Signed-off-by: li-benson <1260437731@qq.com> Signed-off-by: youngmore1024 Co-authored-by: youngmore1024 --- src/db.c | 2 +- tests/unit/pause.tcl | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/db.c b/src/db.c index 8f9a436b4..58aa2eab5 100644 --- a/src/db.c +++ b/src/db.c @@ -333,7 +333,7 @@ robj *dbRandomKey(redisDb *db) { key = dictGetKey(de); keyobj = createStringObject(key,sdslen(key)); if (dictFind(db->expires,key)) { - if (allvolatile && server.masterhost && --maxtries == 0) { + if (allvolatile && (server.masterhost || isPausedActions(PAUSE_ACTION_EXPIRE)) && --maxtries == 0) { /* If the DB is composed only of keys with an expire set, * it could happen that all the keys are already logically * expired in the slave, so the function cannot stop because diff --git a/tests/unit/pause.tcl b/tests/unit/pause.tcl index e30f922e6..5f4e92cae 100644 --- a/tests/unit/pause.tcl +++ b/tests/unit/pause.tcl @@ -359,6 +359,26 @@ start_server {tags {"pause network"}} { } {bar2} } + test "Test the randomkey command will not cause the server to get into an infinite loop during the client pause write" { + r flushall + + r multi + r set key value px 3 + r client pause 10000 write + r exec + + after 5 + + wait_for_condition 50 100 { + [r randomkey] == "key" + } else { + fail "execute randomkey failed, caused by the infinite loop" + } + + r client unpause + assert_equal [r randomkey] {} + } + # Make sure we unpause at the end r client unpause } From 779a20058bd1f7764f6a7e62e0f07a734e6ad869 Mon Sep 17 00:00:00 2001 From: Jason <120577760+jdork0@users.noreply.github.com> Date: Sun, 30 Mar 2025 03:15:04 -0400 Subject: [PATCH 08/13] Ignore shardId updates from replica nodes (#13877) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close https://github.com/redis/redis/issues/13868 This bug was introduced by https://github.com/redis/redis/pull/13468 To maintain compatibility with older versions that do not support shardid, when a replica passes a shardid, we also update the master’s shardid accordingly. However, when both the master and replica support shardid, an issue arises: in one moment, the master may pass a shardid, causing us to update both the master and all its replicas to match the master’s shardid. But if the replica later passes a different shardid, we would then update the master’s shardid again, leading to continuous changes in shardid. Regardless of the situation, we always ensure that the replica’s shardid remains consistent with the master’s shardid. --- src/cluster.c | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/cluster.c b/src/cluster.c index a0944bf23..765958a0c 100644 --- a/src/cluster.c +++ b/src/cluster.c @@ -964,30 +964,24 @@ clusterNode *clusterNodeGetSlave(clusterNode *node, int slave_idx) { static void updateShardId(clusterNode *node, const char *shard_id) { if (shard_id && memcmp(node->shard_id, shard_id, CLUSTER_NAMELEN) != 0) { - assignShardIdToNode(node, shard_id, CLUSTER_TODO_SAVE_CONFIG); - - /* If the replica or master does not support shard-id (old version), - * we still need to make our best effort to keep their shard-id consistent. + /* We always make our best effort to keep the shard-id consistent + * between the master and its replicas: * - * 1. Master supports but the replica does not. - * We might first update the replica's shard-id to the master's randomly - * generated shard-id. Then, when the master's shard-id arrives, we must - * also update all its replicas. - * 2. If the master does not support but the replica does. - * We also need to synchronize the master's shard-id with the replica. - * 3. If neither of master and replica supports it. - * The master will have a randomly generated shard-id and will update - * the replica to match the master's shard-id. */ + * 1. When updating the master's shard-id, we simultaneously update the + * shard-id of all its replicas to ensure consistency. + * 2. When updating replica's shard-id, if it differs from its master's shard-id, + * we discard this replica's shard-id and continue using master's shard-id. + * This applies even if the master does not support shard-id, in which + * case we rely on the master's randomly generated shard-id. */ if (node->slaveof == NULL) { + assignShardIdToNode(node, shard_id, CLUSTER_TODO_SAVE_CONFIG); for (int i = 0; i < clusterNodeNumSlaves(node); i++) { clusterNode *slavenode = clusterNodeGetSlave(node, i); if (memcmp(slavenode->shard_id, shard_id, CLUSTER_NAMELEN) != 0) assignShardIdToNode(slavenode, shard_id, CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_FSYNC_CONFIG); } - } else { - clusterNode *masternode = node->slaveof; - if (memcmp(masternode->shard_id, shard_id, CLUSTER_NAMELEN) != 0) - assignShardIdToNode(masternode, shard_id, CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_FSYNC_CONFIG); + } else if (memcmp(node->slaveof->shard_id, shard_id, CLUSTER_NAMELEN) == 0) { + assignShardIdToNode(node, shard_id, CLUSTER_TODO_SAVE_CONFIG); } } } From 9af9c4deff7699b920ebb6d5ab7f38e0e2a8045e Mon Sep 17 00:00:00 2001 From: YaacovHazan Date: Tue, 22 Apr 2025 13:41:43 +0300 Subject: [PATCH 09/13] Avoid sanitizer warning for stable CI --- src/Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Makefile b/src/Makefile index ecbd2753d..6a8790941 100644 --- a/src/Makefile +++ b/src/Makefile @@ -101,6 +101,9 @@ else ifeq ($(SANITIZER),undefined) MALLOC=libc CFLAGS+=-fsanitize=undefined -fno-sanitize-recover=all -fno-omit-frame-pointer + ifeq (clang,$(CLANG)) + CFLAGS+=-fno-sanitize=function + endif LDFLAGS+=-fsanitize=undefined else ifeq ($(SANITIZER),thread) From 067a0dac61f559a253c985fd411c43944366caaa Mon Sep 17 00:00:00 2001 From: Vitah Lin Date: Mon, 21 Apr 2025 15:23:03 +0800 Subject: [PATCH 10/13] Fix oldTC CI dk.archive.ubuntu.com could not connect (#13961) --- .github/workflows/ci.yml | 4 ++-- .github/workflows/daily.yml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1107221b7..1d5847389 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,8 +91,8 @@ jobs: run: | apt-get update apt-get install -y gnupg2 - echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list - echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list + echo "deb http://archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list + echo "deb http://archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5 apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 apt-get update diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 20c6cf55e..7f88ebd89 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -1116,8 +1116,8 @@ jobs: run: | apt-get update apt-get install -y gnupg2 - echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list - echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list + echo "deb http://archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list + echo "deb http://archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5 apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 apt-get update @@ -1164,8 +1164,8 @@ jobs: run: | apt-get update apt-get install -y gnupg2 - echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list - echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list + echo "deb http://archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list + echo "deb http://archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5 apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 apt-get update @@ -1218,8 +1218,8 @@ jobs: run: | apt-get update apt-get install -y gnupg2 - echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list - echo "deb http://dk.archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list + echo "deb http://archive.ubuntu.com/ubuntu/ xenial main" >> /etc/apt/sources.list + echo "deb http://archive.ubuntu.com/ubuntu/ xenial universe" >> /etc/apt/sources.list apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 40976EAF437D05B5 apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 apt-get update From 21d5e64ace9df3f3f8223f6b6f591efb2663b1a9 Mon Sep 17 00:00:00 2001 From: guybe7 Date: Thu, 11 Jul 2024 16:13:38 +0800 Subject: [PATCH 11/13] Module unblock on keys: updateStatsOnUnblock is called twice (#13405) This commit reverts the deletion of the condition `!bc->blocked_on_keys` that was accidentally introduced by https://github.com/redis/redis/pull/12817. In case a blocked-on-keys module client is unblocked both `moduleUnblockClientOnKey` and `moduleHandleBlockedClients` are called which resulted in `updateStatsOnUnblock` being called twice Now, that `moduleHandleBlockedClients` doesn't call `updateStatsOnUnblock` in case of unblocked module key-blocked clients, in the unlikely event that the module decides to call `RM_UnblockClient` on a key-blocked client, we need to call `updateStatsOnUnblock` from within `moduleBlockedClientTimedOut`, but since `moduleBlockedClientTimedOut` is not tread-safe we can't call it directly from withing `RM_UnblockClient`. Added a new flag `blocked_on_keys_explicit_unblock` for that specific case, which will cause `moduleBlockedClientTimedOut` to be called from `moduleHandleBlockedClients` (which is only called from the main thread) --------- Co-authored-by: debing.sun --- src/blocked.c | 2 +- src/module.c | 44 ++++++++++++++++++++------------------------ src/server.h | 2 +- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/blocked.c b/src/blocked.c index 1bce1eaa7..7b48fcab6 100644 --- a/src/blocked.c +++ b/src/blocked.c @@ -239,7 +239,7 @@ void replyToBlockedClientTimedOut(client *c) { addReplyLongLong(c,server.fsynced_reploff >= c->bstate.reploffset); addReplyLongLong(c,replicationCountAOFAcksByOffset(c->bstate.reploffset)); } else if (c->bstate.btype == BLOCKED_MODULE) { - moduleBlockedClientTimedOut(c, 0); + moduleBlockedClientTimedOut(c); } else { serverPanic("Unknown btype in replyToBlockedClientTimedOut()."); } diff --git a/src/module.c b/src/module.c index f679c0f33..d58918201 100644 --- a/src/module.c +++ b/src/module.c @@ -282,6 +282,10 @@ typedef struct RedisModuleBlockedClient { monotime background_timer; /* Timer tracking the start of background work */ uint64_t background_duration; /* Current command background time duration. Used for measuring latency of blocking cmds */ + int blocked_on_keys_explicit_unblock; /* Set to 1 only in the case of an explicit RM_Unblock on + * a client that is blocked on keys. In this case we will + * call the timeout call back from within + * moduleHandleBlockedClients which runs from the main thread */ } RedisModuleBlockedClient; /* This is a list of Module Auth Contexts. Each time a Module registers a callback, a new ctx is @@ -7720,7 +7724,7 @@ RedisModuleBlockedClient *moduleBlockClient(RedisModuleCtx *ctx, RedisModuleCmdF int islua = scriptIsRunning(); int ismulti = server.in_exec; - c->bstate.module_blocked_handle = zmalloc(sizeof(RedisModuleBlockedClient)); + c->bstate.module_blocked_handle = zcalloc(sizeof(RedisModuleBlockedClient)); RedisModuleBlockedClient *bc = c->bstate.module_blocked_handle; ctx->module->blocked_clients++; @@ -8199,7 +8203,7 @@ int RM_UnblockClient(RedisModuleBlockedClient *bc, void *privdata) { * argument, but better to be safe than sorry. */ if (bc->timeout_callback == NULL) return REDISMODULE_ERR; if (bc->unblocked) return REDISMODULE_OK; - if (bc->client) moduleBlockedClientTimedOut(bc->client, 1); + if (bc->client) bc->blocked_on_keys_explicit_unblock = 1; } moduleUnblockClientByHandle(bc,privdata); return REDISMODULE_OK; @@ -8277,6 +8281,10 @@ void moduleHandleBlockedClients(void) { reply_us = elapsedUs(replyTimer); moduleFreeContext(&ctx); } + if (c && bc->blocked_on_keys_explicit_unblock) { + serverAssert(bc->blocked_on_keys); + moduleBlockedClientTimedOut(c); + } /* Hold onto the blocked client if module auth is in progress. The reply callback is invoked * when the client is reprocessed. */ if (c && clientHasModuleAuthInProgress(c)) { @@ -8297,11 +8305,12 @@ void moduleHandleBlockedClients(void) { /* Update stats now that we've finished the blocking operation. * This needs to be out of the reply callback above given that a * module might not define any callback and still do blocking ops. + * + * If the module is blocked on keys updateStatsOnUnblock will be + * called from moduleUnblockClientOnKey */ - if (c && !clientHasModuleAuthInProgress(c)) { - int had_errors = c->deferred_reply_errors ? !!listLength(c->deferred_reply_errors) : - (server.stat_total_error_replies != prev_error_replies); - updateStatsOnUnblock(c, bc->background_duration, reply_us, had_errors); + if (c && !clientHasModuleAuthInProgress(c) && !bc->blocked_on_keys) { + updateStatsOnUnblock(c, bc->background_duration, reply_us, server.stat_total_error_replies != prev_error_replies); } if (c != NULL) { @@ -8356,37 +8365,24 @@ int moduleBlockedClientMayTimeout(client *c) { * does not need to do any cleanup. Eventually the module will call the * API to unblock the client and the memory will be released. * - * If this function is called from a module, we handle the timeout callback - * and the update of the unblock status in a thread-safe manner to avoid race - * conditions with the main thread. - * If this function is called from the main thread, we must handle the unblocking + * This function should only be called from the main thread, we must handle the unblocking * of the client synchronously. This ensures that we can reply to the client before * resetClient() is called. */ -void moduleBlockedClientTimedOut(client *c, int from_module) { +void moduleBlockedClientTimedOut(client *c) { RedisModuleBlockedClient *bc = c->bstate.module_blocked_handle; - /* Protect against re-processing: don't serve clients that are already - * in the unblocking list for any reason (including RM_UnblockClient() - * explicit call). See #6798. */ - if (bc->unblocked) return; - RedisModuleCtx ctx; - int flags = REDISMODULE_CTX_BLOCKED_TIMEOUT; - if (from_module) flags |= REDISMODULE_CTX_THREAD_SAFE; - moduleCreateContext(&ctx, bc->module, flags); + moduleCreateContext(&ctx, bc->module, REDISMODULE_CTX_BLOCKED_TIMEOUT); ctx.client = bc->client; ctx.blocked_client = bc; ctx.blocked_privdata = bc->privdata; - long long prev_error_replies; - if (!from_module) - prev_error_replies = server.stat_total_error_replies; + long long prev_error_replies = server.stat_total_error_replies; bc->timeout_callback(&ctx,(void**)c->argv,c->argc); moduleFreeContext(&ctx); - if (!from_module) - updateStatsOnUnblock(c, bc->background_duration, 0, server.stat_total_error_replies != prev_error_replies); + updateStatsOnUnblock(c, bc->background_duration, 0, server.stat_total_error_replies != prev_error_replies); /* For timeout events, we do not want to call the disconnect callback, * because the blocked client will be automatically disconnected in diff --git a/src/server.h b/src/server.h index 147f730f4..586711e22 100644 --- a/src/server.h +++ b/src/server.h @@ -2483,7 +2483,7 @@ void moduleFreeContext(struct RedisModuleCtx *ctx); void moduleCallCommandUnblockedHandler(client *c); void unblockClientFromModule(client *c); void moduleHandleBlockedClients(void); -void moduleBlockedClientTimedOut(client *c, int from_module); +void moduleBlockedClientTimedOut(client *c); void modulePipeReadable(aeEventLoop *el, int fd, void *privdata, int mask); size_t moduleCount(void); void moduleAcquireGIL(void); From 42fb340ce426364d64f5dccc9c2549e58f48ac6f Mon Sep 17 00:00:00 2001 From: YaacovHazan Date: Wed, 23 Apr 2025 08:09:40 +0000 Subject: [PATCH 12/13] Limiting output buffer for unauthenticated client (CVE-2025-21605) For unauthenticated clients the output buffer is limited to prevent them from abusing it by not reading the replies --- src/networking.c | 5 +++++ tests/unit/auth.tcl | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/networking.c b/src/networking.c index 7007141a8..bb1a72b9f 100644 --- a/src/networking.c +++ b/src/networking.c @@ -3858,6 +3858,11 @@ int checkClientOutputBufferLimits(client *c) { int soft = 0, hard = 0, class; unsigned long used_mem = getClientOutputBufferMemoryUsage(c); + /* For unauthenticated clients the output buffer is limited to prevent + * them from abusing it by not reading the replies */ + if (used_mem > 1024 && authRequired(c)) + return 1; + class = getClientType(c); /* For the purpose of output buffer limiting, masters are handled * like normal clients. */ diff --git a/tests/unit/auth.tcl b/tests/unit/auth.tcl index 9532e0bd2..0d25f5dea 100644 --- a/tests/unit/auth.tcl +++ b/tests/unit/auth.tcl @@ -45,6 +45,24 @@ start_server {tags {"auth external:skip"} overrides {requirepass foobar}} { assert_match {*unauthenticated bulk length*} $e $rr close } + + test {For unauthenticated clients output buffer is limited} { + set rr [redis [srv "host"] [srv "port"] 1 $::tls] + $rr SET x 5 + catch {[$rr read]} e + assert_match {*NOAUTH Authentication required*} $e + + # Fill the output buffer in a loop without reading it and make + # sure the client disconnected. + # Considering the socket eat some of the replies, we are testing + # that such client can't consume more than few MB's. + catch { + for {set j 0} {$j < 1000000} {incr j} { + $rr SET x 5 + } + } e + assert_match {I/O error reading reply} $e + } } start_server {tags {"auth_binary_password external:skip"}} { From 62b766aef6173a77f06d9779369ff025d4d391d9 Mon Sep 17 00:00:00 2001 From: YaacovHazan Date: Wed, 23 Apr 2025 08:11:54 +0000 Subject: [PATCH 13/13] Redis 7.2.8 --- 00-RELEASENOTES | 16 ++++++++++++++++ src/version.h | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/00-RELEASENOTES b/00-RELEASENOTES index 569e0cb28..1d14c0415 100644 --- a/00-RELEASENOTES +++ b/00-RELEASENOTES @@ -11,6 +11,22 @@ CRITICAL: There is a critical bug affecting MOST USERS. Upgrade ASAP. SECURITY: There are security fixes in the release. -------------------------------------------------------------------------------- +================================================================================ +Redis 7.2.8 Released Wed 23 Apr 2025 12:00:00 IST +================================================================================ + +Update urgency: `SECURITY`: There are security fixes in the release. + +### Security fixes + +* (CVE-2025-21605) An unauthenticated client can cause an unlimited growth of output buffers + +### Bug fixes + +* #12817, #12905 Fix race condition issues between the main thread and module threads +* #13863 `RANDOMKEY` - infinite loop during client pause +* #13877 ShardID inconsistency when both primary and replica support it + ================================================================================ Redis 7.2.7 Released Mon 6 Jan 2025 12:30:00 IDT diff --git a/src/version.h b/src/version.h index 6958e84ca..9d41c5ab1 100644 --- a/src/version.h +++ b/src/version.h @@ -1,2 +1,2 @@ -#define REDIS_VERSION "7.2.7" -#define REDIS_VERSION_NUM 0x00070207 +#define REDIS_VERSION "7.2.8" +#define REDIS_VERSION_NUM 0x00070208