Skip to content

Fix global cache - #9

Open
chejinge wants to merge 3 commits into
pikiwidb:mainfrom
chejinge:fix_global_cache
Open

Fix global cache#9
chejinge wants to merge 3 commits into
pikiwidb:mainfrom
chejinge:fix_global_cache

Conversation

@chejinge

@chejinge chejinge commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Refactor

    • Eviction and memory-management now operate across all databases for more consistent multi-database resource handling.
  • Bug Fixes

    • Eviction now removes keys from the correct source database, improving eviction reliability.
  • Documentation

    • Added README describing the RedisCache module.
  • Chores

    • Added diagnostic logging during hash iteration to aid debugging.

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85c1f1f5-a37c-4c03-9854-e15ae34d2b06

📥 Commits

Reviewing files that changed from the base of the PR and between aff8d5b and a2e4e2e.

📒 Files selected for processing (1)
  • README.md
✅ Files skipped from review due to trivial changes (1)
  • README.md

📝 Walkthrough

Walkthrough

Eviction was refactored to track multiple redisDb instances globally. Eviction pool entries now record source DBs, eviction routines iterate over a global DB list to populate/select candidates, and RcCreateCacheHandle registers new DBs in that global list.

Changes

Cohort / File(s) Summary
Eviction Infrastructure
evict.h, evict.c
Added void *db to struct evictionPoolEntry; updated evictionPoolPopulate() to accept a void *db parameter and store DB context in pool entries.
Eviction Logic
db.c
Refactored freeMemoryIfNeeded() signature to freeMemoryIfNeeded(redisDb *trigger_db) and changed eviction selection to iterate g_all_redis_dbs (guarded by g_db_list_mutex), using pool[k].db/bestdb to delete keys from the correct DB.
Database Registration
redis.c
RcCreateCacheHandle() now creates a redisDb, registers it into global array g_all_redis_dbs under g_db_list_mutex, and increments g_redis_db_num up to MAX_CACHE_DB_NUM.
Debug Output
t_hash.c
Added #include "stdio.h" and two printf debug statements inside genericHgetall() to log hash field/value during iteration.
Docs
README.md
Added new README with title "RedisCache" and a short description of the module.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RcCreateCacheHandle as RcCreateCacheHandle()
    participant GlobalDBList as Global DB List (g_all_redis_dbs)
    participant MemoryCheck as freeMemoryIfNeeded()
    participant EvictionPool as evictionPoolPopulate()
    participant TargetDB as Target DB (from pool)

    Client->>RcCreateCacheHandle: create cache handle
    RcCreateCacheHandle->>GlobalDBList: lock g_db_list_mutex / register new DB
    GlobalDBList-->>RcCreateCacheHandle: unlock / return handle

    Client->>MemoryCheck: freeMemoryIfNeeded(trigger_db)
    MemoryCheck->>GlobalDBList: lock g_db_list_mutex
    MemoryCheck->>GlobalDBList: iterate g_all_redis_dbs
    loop per registered DB
        MemoryCheck->>EvictionPool: evictionPoolPopulate(sampledict, keydict, pool, db)
        Note over EvictionPool: pool entries store .db -> source DB
    end
    MemoryCheck->>MemoryCheck: select bestkey & bestdb from pool
    MemoryCheck->>TargetDB: delete bestkey from bestdb
    TargetDB-->>MemoryCheck: deletion result
    MemoryCheck-->>Client: return eviction outcome
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through lists both wide and deep,

I tucked each key where memories keep,
Pools now whisper which DB to spare,
Together they lighten the caching lair,
A little hop, a tidy sweep.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix global cache' is vague and does not clearly describe the specific changes made, which involve refactoring eviction logic to work across multiple databases using global database lists. Consider a more descriptive title such as 'Refactor memory eviction to support multi-database selection' or 'Add global database tracking for eviction pool management' to better convey the scope of changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@db.c`:
- Around line 457-466: The loop currently picks the first non-empty DB and
samples a random key only from it, biasing evictions; instead, under
MAXMEMORY_ALLKEYS_RANDOM or volatile-random policy compute each DB's eligible
key count using dictSize(curr_db->dict) or dictSize(curr_db->expires) (based on
maxmemory_policy), sum them to total, pick a single uniform random index in
[0,total-1], iterate the DBs subtracting each eligible size until the random
index falls into a DB, then call dictGetRandomKey on that chosen DB and set
bestdb/bestkey/de accordingly; keep the existing pthread_mutex_lock/unlock and
handle the total==0 case by returning/continuing as before.
- Around line 422-430: The code unlocks g_db_list_mutex before using/deleting
the selected redisDb (bestdb), allowing RcDestroyCacheHandle() to free it
concurrently; fix by ensuring the DB is pinned for the entire select+delete
sequence: after reading a redisDb* from g_all_redis_dbs (inside the
g_db_list_mutex) call the appropriate pin/retain API (so it cannot be
destroyed—e.g., a RcHold/retain function corresponding to RcDestroyCacheHandle)
or otherwise increment its reference count, then unlock the mutex and perform
evictionPoolPopulate/select+delete, and finally unpin/release the DB;
alternatively, keep g_db_list_mutex held across the full lifetime-sensitive
section (for functions evictionPoolPopulate and any code that dereferences
bestdb) so the DB cannot be freed while in use. Ensure to apply the same change
to the other blocks mentioned (lines analogous to 435-447, 457-468, 471-475)
referencing g_all_redis_dbs, g_db_list_mutex, evictionPoolPopulate, bestdb and
RcDestroyCacheHandle.

In `@redis.c`:
- Around line 71-79: RcCreateCacheHandle() registers every new redisDb* in the
global registry (g_all_redis_dbs/g_redis_db_num) but RcDestroyCacheHandle() only
frees the db, leaving stale pointers and causing use-after-free during registry
iteration; modify RcDestroyCacheHandle() to, under the g_db_list_mutex, find the
matching redisDb* in g_all_redis_dbs, remove it (either shift subsequent entries
down or replace it with the last entry), decrement g_redis_db_num, clear the
removed slot (set NULL if applicable), then unlock before freeing the db; ensure
you handle the case where the pointer isn’t found gracefully.

In `@t_hash.c`:
- Line 13: Remove the stdout debug printing from genericHgetall: delete the
printf/puts calls that print each hash field/value (the prints around lines
~599-606 in function genericHgetall) and remove the `#include` "stdio.h" at the
top (line 13) since it was only pulled in for those debug prints; if you still
need diagnostics, replace them with the project's non-blocking logging facility
rather than stdout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bafff50-1829-4c89-8b62-1cea1b08355c

📥 Commits

Reviewing files that changed from the base of the PR and between b280516 and aff8d5b.

📒 Files selected for processing (5)
  • db.c
  • evict.c
  • evict.h
  • redis.c
  • t_hash.c

Comment thread db.c
Comment on lines +422 to +430
pthread_mutex_lock(&g_db_list_mutex);
for (int i = 0; i < g_redis_db_num; i++) {
redisDb *curr_db = (redisDb *)g_all_redis_dbs[i];
dict *d = (maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ? curr_db->dict : curr_db->expires;
if (dictSize(d) != 0) {
evictionPoolPopulate(d, curr_db->dict, pool, curr_db);
}
}
if (!keys) break; /* No keys to evict. */
pthread_mutex_unlock(&g_db_list_mutex);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

The registry mutex does not protect bestdb long enough.

Both eviction paths read redisDb * values from g_all_redis_dbs while holding g_db_list_mutex, then dereference and delete through bestdb after unlocking. A concurrent RcDestroyCacheHandle() can free that DB in between, so this can walk or delete through a dangling pointer. The DB needs to stay pinned for the full select+delete sequence, or the lock needs to cover the whole lifetime-sensitive section.

Also applies to: 435-447, 457-468, 471-475

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db.c` around lines 422 - 430, The code unlocks g_db_list_mutex before
using/deleting the selected redisDb (bestdb), allowing RcDestroyCacheHandle() to
free it concurrently; fix by ensuring the DB is pinned for the entire
select+delete sequence: after reading a redisDb* from g_all_redis_dbs (inside
the g_db_list_mutex) call the appropriate pin/retain API (so it cannot be
destroyed—e.g., a RcHold/retain function corresponding to RcDestroyCacheHandle)
or otherwise increment its reference count, then unlock the mutex and perform
evictionPoolPopulate/select+delete, and finally unpin/release the DB;
alternatively, keep g_db_list_mutex held across the full lifetime-sensitive
section (for functions evictionPoolPopulate and any code that dereferences
bestdb) so the DB cannot be freed while in use. Ensure to apply the same change
to the other blocks mentioned (lines analogous to 435-447, 457-468, 471-475)
referencing g_all_redis_dbs, g_db_list_mutex, evictionPoolPopulate, bestdb and
RcDestroyCacheHandle.

Comment thread db.c
Comment on lines +457 to +466
pthread_mutex_lock(&g_db_list_mutex);
for (int i = 0; i < g_redis_db_num; i++) {
redisDb *curr_db = (redisDb *)g_all_redis_dbs[i];
dict *d = (maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ? curr_db->dict : curr_db->expires;
if (dictSize(d) != 0) {
de = dictGetRandomKey(d);
bestkey = dictGetKey(de);
bestdb = curr_db;
break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Random eviction is now biased toward the first non-empty DB.

This loop stops at the first eligible DB and then samples a random key only inside that DB. With multiple registered DBs, lower-index DBs will absorb nearly all allkeys-random / volatile-random evictions, which is not random over the global keyspace. Pick the DB proportionally to its eligible key count, or sample from the combined candidate set before deleting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db.c` around lines 457 - 466, The loop currently picks the first non-empty DB
and samples a random key only from it, biasing evictions; instead, under
MAXMEMORY_ALLKEYS_RANDOM or volatile-random policy compute each DB's eligible
key count using dictSize(curr_db->dict) or dictSize(curr_db->expires) (based on
maxmemory_policy), sum them to total, pick a single uniform random index in
[0,total-1], iterate the DBs subtracting each eligible size until the random
index falls into a DB, then call dictGetRandomKey on that chosen DB and set
bestdb/bestkey/de accordingly; keep the existing pthread_mutex_lock/unlock and
handle the total==0 case by returning/continuing as before.

Comment thread redis.c
Comment on lines +71 to +79
redisDb *db = createRedisDb();
if (db) {
pthread_mutex_lock(&g_db_list_mutex);
if (g_redis_db_num < MAX_CACHE_DB_NUM) {
g_all_redis_dbs[g_redis_db_num++] = db;
}
pthread_mutex_unlock(&g_db_list_mutex);
}
return db;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unregister destroyed DBs from the global registry.

RcCreateCacheHandle() now publishes every redisDb *, but RcDestroyCacheHandle() still just frees it. That leaves freed pointers in g_all_redis_dbs, keeps g_redis_db_num growing forever, and eventually returns live handles that are never registered once the array fills. Since db.c now iterates this registry during eviction, stale entries turn into a use-after-free path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@redis.c` around lines 71 - 79, RcCreateCacheHandle() registers every new
redisDb* in the global registry (g_all_redis_dbs/g_redis_db_num) but
RcDestroyCacheHandle() only frees the db, leaving stale pointers and causing
use-after-free during registry iteration; modify RcDestroyCacheHandle() to,
under the g_db_list_mutex, find the matching redisDb* in g_all_redis_dbs, remove
it (either shift subsequent entries down or replace it with the last entry),
decrement g_redis_db_num, clear the removed slot (set NULL if applicable), then
unlock before freeing the db; ensure you handle the case where the pointer isn’t
found gracefully.

Comment thread t_hash.c
#include "db.h"
#include "ziplist.h"
#include "util.h"
#include "stdio.h"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove stdout debug logging from genericHgetall before merge.

Line 601 and Line 605 print every hash field/value to stdout. This introduces data-leak risk and adds blocking I/O in a frequently used path. Line 13 is only needed for this debug output.

Proposed fix
-#include "stdio.h"
@@
         if (flags & OBJ_HASH_KEY) {
             addHashIteratorCursorToReply(hi, OBJ_HASH_KEY, &((*items+i)->field));
-            printf("field: %s\n", (*items+i)->field);
         }
         if (flags & OBJ_HASH_VALUE) {
             addHashIteratorCursorToReply(hi, OBJ_HASH_VALUE, &((*items+i)->value));
-            printf("value: %s\n", (*items+i)->value);
         }

Also applies to: 599-606

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@t_hash.c` at line 13, Remove the stdout debug printing from genericHgetall:
delete the printf/puts calls that print each hash field/value (the prints around
lines ~599-606 in function genericHgetall) and remove the `#include` "stdio.h" at
the top (line 13) since it was only pulled in for those debug prints; if you
still need diagnostics, replace them with the project's non-blocking logging
facility rather than stdout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants