Skip to content

Added changes to propogate FLUSHSLOT instead of UNLINK in replication stream in case of ASM slot migration#4191

Open
omanges wants to merge 2 commits into
valkey-io:unstablefrom
omanges:asm_opti
Open

Added changes to propogate FLUSHSLOT instead of UNLINK in replication stream in case of ASM slot migration#4191
omanges wants to merge 2 commits into
valkey-io:unstablefrom
omanges:asm_opti

Conversation

@omanges

@omanges omanges commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Added changes to propogate FLUSHSLOT instead of UNLINK in replication stream in case of ASM slot migration

#3405

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Cluster flush-slot propagation

Layer / File(s) Summary
Update deletion contract and cleanup callers
src/cluster.h, src/cluster_legacy.c, src/cluster_migrateslots.c, src/cluster.c
delKeysInSlot gains a propagation-mode parameter, and slot cleanup callers pass the appropriate value.
Implement flush-slot deletion propagation
src/cluster_legacy.c
Slot deletion can propagate CLUSTER FLUSHSLOT, suppress per-key propagation, snapshot keys before deletion, and free the snapshot.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: sarthakaggarwal97

Sequence Diagram(s)

sequenceDiagram
  participant ClusterCleanup
  participant delKeysInSlot
  participant alsoPropagate
  participant Keyspace
  ClusterCleanup->>delKeysInSlot: delete keys with flush-slot propagation enabled
  delKeysInSlot->>alsoPropagate: propagate CLUSTER FLUSHSLOT
  delKeysInSlot->>Keyspace: snapshot and delete slot keys
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly reflects the main change: propagating FLUSHSLOT during ASM slot migration instead of UNLINK.
Description check ✅ Passed The description is directly related to the changeset and matches the stated FLUSHSLOT replication-stream update.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

… stream in case of ASM slot migration

Signed-off-by: Omkar Mestry <om.m.mestry@gmail.com>

@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.

🧹 Nitpick comments (1)
src/cluster.h (1)

152-152: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unify parameter naming between declaration and definition.

The parameter is named propagate_as_flushslot here in the declaration, but in src/cluster_legacy.c, it is defined as is_asm_path. Consider aligning the names for consistency.

♻️ Proposed refactor
-unsigned int delKeysInSlot(unsigned int hashslot, int lazy, bool propagate_del, bool send_del_event, bool propagate_as_flushslot);
+unsigned int delKeysInSlot(unsigned int hashslot, int lazy, bool propagate_del, bool send_del_event, bool is_asm_path);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster.h` at line 152, Rename the mismatched parameter in the definition
of delKeysInSlot to propagate_as_flushslot so it matches the declaration in
cluster.h; preserve the existing behavior and call-site semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/cluster.h`:
- Line 152: Rename the mismatched parameter in the definition of delKeysInSlot
to propagate_as_flushslot so it matches the declaration in cluster.h; preserve
the existing behavior and call-site semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c08e2de6-f4ae-4088-90bb-9026d3f6dbd0

📥 Commits

Reviewing files that changed from the base of the PR and between 79fc841 and 6bc9f19.

📒 Files selected for processing (4)
  • src/cluster.c
  • src/cluster.h
  • src/cluster_legacy.c
  • src/cluster_migrateslots.c

@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.

🧹 Nitpick comments (2)
src/cluster_legacy.c (2)

7533-7533: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align parameter name with header declaration.

The parameter is named is_asm_path here, but it is declared as propagate_as_flushslot in src/cluster.h (as shown in the provided context).

Consider renaming it to propagate_as_flushslot for consistency. This name is also semantically stronger because it describes the desired propagation behavior rather than leaking the caller's context into the function.

♻️ Proposed refactor
-unsigned int delKeysInSlot(unsigned int hashslot, int lazy, bool propagate_del, bool send_del_event, bool is_asm_path) {
+unsigned int delKeysInSlot(unsigned int hashslot, int lazy, bool propagate_del, bool send_del_event, bool propagate_as_flushslot) {

(Note: If you apply this change, remember to also update the is_asm_path references on lines 7559 and 7579 within this function).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_legacy.c` at line 7533, Rename the delKeysInSlot parameter
is_asm_path to propagate_as_flushslot to match the cluster.h declaration, and
update all references to that parameter within delKeysInSlot.

7511-7512: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mark helper function as static.

Since propagateAsmFlushSlot is only used internally within this file and isn't exported in the headers, it should be marked static to prevent namespace pollution and potentially allow the compiler to inline it.

♻️ Proposed refactor
 /* Helper function to propagate the CLUSTER FLUSHSLOT command to replicas and AOF during ASM. */
-void propagateAsmFlushSlot(unsigned int hashslot, int lazy) {
+static void propagateAsmFlushSlot(unsigned int hashslot, int lazy) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_legacy.c` around lines 7511 - 7512, Mark the file-local helper
function propagateAsmFlushSlot as static in its definition, preserving its
existing parameters and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/cluster_legacy.c`:
- Line 7533: Rename the delKeysInSlot parameter is_asm_path to
propagate_as_flushslot to match the cluster.h declaration, and update all
references to that parameter within delKeysInSlot.
- Around line 7511-7512: Mark the file-local helper function
propagateAsmFlushSlot as static in its definition, preserving its existing
parameters and behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 65bc8f08-b81b-42aa-8d2c-d809751809ff

📥 Commits

Reviewing files that changed from the base of the PR and between 6bc9f19 and ccb2a50.

📒 Files selected for processing (4)
  • src/cluster.c
  • src/cluster.h
  • src/cluster_legacy.c
  • src/cluster_migrateslots.c
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/cluster.h
  • src/cluster_migrateslots.c
  • src/cluster.c

@omanges

omanges commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

I was trying to add test but facing issue, so raised changes for review by the time I fix the test, not sure am I using attach_to_replication_stream_on_connection correctly :-

This is an example test I am trying to add :-

test "Atomic Slot Migration cleanup propagates CLUSTER FLUSHSLOT to replication stream on successful migration" {
        # Populate data before migration
        populate 1000 "$16383_slot_tag:" 1000 -2

        set repl [attach_to_replication_stream_on_connection -2]

        # Perform one-shot import
        assert_match "OK" [R 2 CLUSTER MIGRATESLOTS SLOTSRANGE 16383 16383 NODE $node0_id]
        set jobname [get_job_name 2 16383]
        wait_for_migration 0 16383

        # Keys successfully migrated
        assert_match "1000" [R 0 CLUSTER COUNTKEYSINSLOT 16383]
        assert_match "0" [R 2 CLUSTER COUNTKEYSINSLOT 16383]

        # Also eventually reflected in replicas
        wait_for_countkeysinslot 3 16383 1000
        wait_for_countkeysinslot 5 16383 0

        # Migration log shows success on both ends
        assert {[dict get [get_migration_by_name 0 $jobname] state] eq "success"}
        assert {[dict get [get_migration_by_name 2 $jobname] state] eq "success"}

        # 4. Read replication stream of Node 2 to capture emitted CLUSTER FLUSHSLOT
        set found_flushslot 0
        set found_individual_dels 0
        set empty_count 0
        while {$empty_count < 10} {
            set cmd [read_from_replication_stream $repl]
            if {$cmd eq ""} {
                incr empty_count
                after 100
                continue
            }
            set empty_count 0
            # Print each command to verify stream output
            puts "REPL CMD: $cmd"
            if {[string match -nocase "*flushslot*" $cmd]} {
                set found_flushslot 1
                break
            }
            if {[string match -nocase "*unlink*" $cmd] || [string match -nocase "*del*" $cmd]} {
                set found_individual_dels 1
            }
        }

        # Assert CLUSTER FLUSHSLOT was emitted on Node 2 and individual DEL/UNLINKs were avoided
        assert_equal 1 $found_flushslot
        assert_equal 0 $found_individual_dels

        close_replication_stream $repl

        # Cleanup for next test: move slot 16383 back to Node 2
        assert_match "OK" [R 0 FLUSHDB SYNC]
        assert_match "OK" [R 0 CLUSTER MIGRATESLOTS SLOTSRANGE 16383 16383 NODE $node2_id]
        wait_for_migration 2 16383
    }

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.85%. Comparing base (7bb7b7f) to head (8aa898e).
⚠️ Report is 10 commits behind head on unstable.

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #4191      +/-   ##
============================================
+ Coverage     76.83%   76.85%   +0.02%     
============================================
  Files           162      162              
  Lines         81433    81499      +66     
============================================
+ Hits          62565    62639      +74     
+ Misses        18868    18860       -8     
Files with missing lines Coverage Δ
src/cluster.c 92.42% <100.00%> (ø)
src/cluster_legacy.c 88.04% <100.00%> (-0.01%) ⬇️
src/cluster_migrateslots.c 92.17% <100.00%> (+0.18%) ⬆️

... and 24 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@valkey-review-bot valkey-review-bot 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.

I found one correctness issue in the new ASM cleanup propagation path. The detail is inline.

Comment thread src/cluster_legacy.c Outdated
}
// if is command, skip del propagate
if (propagate_del) propagateDeletion(db, key, lazy, hashslot);
if (propagate_del && !is_asm_path) propagateDeletion(db, key, lazy, hashslot);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipping propagateDeletion() here means the per-key execution units at src/cluster_legacy.c:7560-7571 still flush any replicated module/post-notification writes before the later slot-wide FLUSHSLOT is appended. Notification callbacks are allowed to issue writes (tests/modules/keyspace_events.c:84-98), so an ASM-aware module that writes back into the migrating slot from its del callback will now be replayed on replicas/AOF before the flush and then deleted again by the later FLUSHSLOT, even though it survived on the primary. The old per-key propagation kept the delete and those callback side effects ordered together; tests/unit/moduleapi/cluster.tcl:235-266 covers that invariant for slot-change deletions.

Keep the delete propagation ordered with the notification side effects in this path, or fall back to per-key propagation for ASM cleanup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah we need to think this through a bit more:

  1. If we send CLUSTER FLUSHSLOT after, it will flush everything, even items that were recreated by modules -> desync
  2. If we send CLUSTER FLUSHSLOT before, we will only flush the items that existed at the start. But if the module recreates a key, that recreated key may also be iterated a second time (since kvstore iterator does not guarantee we don't iterate over newly added items). If the module doesn't recreate on the second call -> desync

I think the best solution is:

  1. At the beginning, swap in a fresh new hashtable for the keys, expires, etc for that slot in the DB
  2. At the same time, propagate the CLUSTER FLUSHSLOT
  3. Now, iterate over each key in the OLD hashtable and trigger the keyspace notifications
  4. Any recreated keys will land in the NEW hashtable and not be reiterated (don't hit the second issue above)

WDYT?

Another option is to force modules to handle FLUSHSLOT as an event like what we do for FLUSHDB, and not trigger any key-level notifications. But that would require module opt-in, and it may be hard for modules to handle this (e.g. search would need to somehow figure out which keys in the index belong to the slot).

In any case - I think the first option is easier for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, I agree with the suggested solution. Propagating FLUSHSLOT before deleting the keys ensures that any data or metadata created by callback events during key deletion is preserved on replicas rather than being wiped out. I have updated the PR accordingly.

Comment thread src/cluster_legacy.c Outdated
}

/* Helper function to propagate the CLUSTER FLUSHSLOT command to replicas and AOF during ASM. */
void propagateAsmFlushSlot(unsigned int hashslot, int lazy) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: naming

Suggested change
void propagateAsmFlushSlot(unsigned int hashslot, int lazy) {
void propagateFlushSlot(unsigned int hashslot, int lazy) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done updated to propagateFlushSlot

Comment thread src/cluster_legacy.c Outdated
/* Remove all the keys in the specified hash slot.
* The number of removed items is returned. */
unsigned int delKeysInSlot(unsigned int hashslot, int lazy, bool propagate_del, bool send_del_event) {
unsigned int delKeysInSlot(unsigned int hashslot, int lazy, bool propagate_del, bool send_del_event, bool is_asm_path) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: match the naming in cluster.h

Suggested change
unsigned int delKeysInSlot(unsigned int hashslot, int lazy, bool propagate_del, bool send_del_event, bool is_asm_path) {
unsigned int delKeysInSlot(unsigned int hashslot, int lazy, bool propagate_del, bool send_del_event, bool propagate_as_flushslot) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done update to propagate_as_flushslot

if (cleanup_needed) {
/* Defer cleanup until beforeSleep. */
job->post_cleanup_state = state;
state = SLOT_IMPORT_FINISHED_CLEANING_UP;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note that delKeysNotOwnedByMyself is only triggered on the importing node for cleanup. It is half of the required propagation. The other half will need to be in cluster_legacy.c when we clean up unowned slots on the exporting node.

You will need to check in the dirty slot deletion logic whether or not the dirty slot was exporting or not. If it was exporting, you will want to use FLUSHSLOT.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done updated propagate_as_flushslot to true in :-

if (delete_dirty_slots) {
        for (int j = 0; j < dirty_slots_count; j++) {
            serverLog(LL_NOTICE, "Deleting keys in dirty slot %d on node %.40s (%s) in shard %.40s", dirty_slots[j],
                      myself->name, humanNodename(myself), myself->shard_id);
            delKeysInSlot(dirty_slots[j], server.lazyfree_lazy_server_del, true, false, true);
        }
    }``` 

Comment thread src/cluster_legacy.c Outdated
}
// if is command, skip del propagate
if (propagate_del) propagateDeletion(db, key, lazy, hashslot);
if (propagate_del && !is_asm_path) propagateDeletion(db, key, lazy, hashslot);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah we need to think this through a bit more:

  1. If we send CLUSTER FLUSHSLOT after, it will flush everything, even items that were recreated by modules -> desync
  2. If we send CLUSTER FLUSHSLOT before, we will only flush the items that existed at the start. But if the module recreates a key, that recreated key may also be iterated a second time (since kvstore iterator does not guarantee we don't iterate over newly added items). If the module doesn't recreate on the second call -> desync

I think the best solution is:

  1. At the beginning, swap in a fresh new hashtable for the keys, expires, etc for that slot in the DB
  2. At the same time, propagate the CLUSTER FLUSHSLOT
  3. Now, iterate over each key in the OLD hashtable and trigger the keyspace notifications
  4. Any recreated keys will land in the NEW hashtable and not be reiterated (don't hit the second issue above)

WDYT?

Another option is to force modules to handle FLUSHSLOT as an event like what we do for FLUSHDB, and not trigger any key-level notifications. But that would require module opt-in, and it may be hard for modules to handle this (e.g. search would need to somehow figure out which keys in the index belong to the slot).

In any case - I think the first option is easier for now.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cluster_legacy.c (1)

7571-7596: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Check deletion success to prevent TOCTOU state corruption.

Because keys are collected into a snapshot, a key in the snapshot might already be deleted by the time the loop reaches it (e.g., deleted by a module callback triggered by a prior key's deletion in the same loop). Blindly processing the key when dbSyncDelete / dbAsyncDelete fails to find it will cause duplicate keyspace events, double propagation, and corrupted server.dirty statistics.

Check the return value of the delete function and only proceed if the key was actually removed.

🐛 Proposed fix
         /* 3. Delete the snapshot of collected keys. */
         for (unsigned int k = 0; k < keys_count; k++) {
             robj *key = keys_to_del[k];
             enterExecutionUnit(1, 0);
-            if (lazy) {
-                dbAsyncDelete(db, key);
-            } else {
-                dbSyncDelete(db, key);
-            }
-            // if is command, skip del propagate
-            if (propagate_del && !propagate_as_flushslot) propagateDeletion(db, key, lazy, hashslot);
-            signalModifiedKey(NULL, db, key);
-            if (send_del_event) {
-                /* In the `cluster flushslot` scenario, the keys are actually deleted so notify everyone. */
-                notifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, db->id);
-            } else {
-                /* The keys are not actually logically deleted from the database, just moved to another node.
-                 * The modules needs to know that these keys are no longer available locally, so just send the
-                 * keyspace notification to the modules, but not to clients. */
-                moduleNotifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, db->id);
-            }
-            exitExecutionUnit();
-            postExecutionUnitOperations();
-            decrRefCount(key);
-            j++;
-            server.dirty++;
+            int deleted = lazy ? dbAsyncDelete(db, key) : dbSyncDelete(db, key);
+            if (deleted) {
+                // if is command, skip del propagate
+                if (propagate_del && !propagate_as_flushslot) propagateDeletion(db, key, lazy, hashslot);
+                signalModifiedKey(NULL, db, key);
+                if (send_del_event) {
+                    /* In the `cluster flushslot` scenario, the keys are actually deleted so notify everyone. */
+                    notifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, db->id);
+                } else {
+                    /* The keys are not actually logically deleted from the database, just moved to another node.
+                     * The modules needs to know that these keys are no longer available locally, so just send the
+                     * keyspace notification to the modules, but not to clients. */
+                    moduleNotifyKeyspaceEvent(NOTIFY_GENERIC, "del", key, db->id);
+                }
+                j++;
+                server.dirty++;
+            }
+            exitExecutionUnit();
+            postExecutionUnitOperations();
+            decrRefCount(key);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_legacy.c` around lines 7571 - 7596, Check the return value of
dbSyncDelete or dbAsyncDelete in the deletion loop before performing
propagation, key signaling, notifications, reference-count cleanup, incrementing
j, or updating server.dirty. Continue processing only when the deletion actually
succeeds, while preserving the existing lazy/non-lazy delete selection and
execution-unit lifecycle.
🧹 Nitpick comments (1)
src/cluster_legacy.c (1)

7524-7524: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider passing -1 as the dbid for CLUSTER FLUSHSLOT.

CLUSTER FLUSHSLOT is a database-neutral cluster command that flushes the slot across the entire node. Passing dbid = 0 to alsoPropagate will unnecessarily emit a SELECT 0 to the replication stream (if the replica is not already on DB 0). Passing -1 avoids this.

♻️ Proposed refactor
-    alsoPropagate(0, argv, argc, PROPAGATE_REPL | PROPAGATE_AOF, hashslot);
+    alsoPropagate(-1, argv, argc, PROPAGATE_REPL | PROPAGATE_AOF, hashslot);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_legacy.c` at line 7524, Update the alsoPropagate call in the
CLUSTER FLUSHSLOT handling to pass dbid -1 instead of 0, preserving the existing
arguments and propagation flags so the database-neutral command does not emit
SELECT 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cluster_legacy.c`:
- Around line 7559-7568: Update the keys_to_del snapshot construction in the
kvstoreHashtableIterator loop to grow its allocation dynamically as keys_count
approaches capacity, reallocating before writing each new object. Preserve the
existing key-copying and iterator-release behavior while ensuring insertions
during multi-DB processing cannot write beyond the buffer.

In `@tests/unit/cluster/cluster-migrateslots.tcl`:
- Around line 619-637: Remove the early break from the replication-stream loop
that checks found_flushslot and found_individual_dels. After detecting a
FLUSHSLOT command, continue reading until empty_count reaches 50 so subsequent
DEL or UNLINK commands are still detected.

---

Outside diff comments:
In `@src/cluster_legacy.c`:
- Around line 7571-7596: Check the return value of dbSyncDelete or dbAsyncDelete
in the deletion loop before performing propagation, key signaling,
notifications, reference-count cleanup, incrementing j, or updating
server.dirty. Continue processing only when the deletion actually succeeds,
while preserving the existing lazy/non-lazy delete selection and execution-unit
lifecycle.

---

Nitpick comments:
In `@src/cluster_legacy.c`:
- Line 7524: Update the alsoPropagate call in the CLUSTER FLUSHSLOT handling to
pass dbid -1 instead of 0, preserving the existing arguments and propagation
flags so the database-neutral command does not emit SELECT 0.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c54e31f-0bdb-403a-87e1-d9761f677cf7

📥 Commits

Reviewing files that changed from the base of the PR and between ccb2a50 and c68afc2.

📒 Files selected for processing (2)
  • src/cluster_legacy.c
  • tests/unit/cluster/cluster-migrateslots.tcl

Comment thread src/cluster_legacy.c Outdated
Comment on lines +7559 to +7568
robj **keys_to_del = zmalloc(sizeof(robj*) * num_keys);
unsigned int keys_count = 0;

kvstoreHashtableIterator *kvs_di = kvstoreGetHashtableIterator(db->keys, hashslot, HASHTABLE_ITER_SAFE);
void *next;
while (kvstoreHashtableIteratorNext(kvs_di, &next)) {
robj *valkey = next;
sds sdskey = objectGetKey((robj*)next);
keys_to_del[keys_count++] = createStringObject(sdskey, sdslen(sdskey));
}
kvstoreReleaseHashtableIterator(kvs_di);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Prevent buffer overflow when allocating keys_to_del.

The size of keys_to_del is based on num_keys, which is the total key count captured before the multi-DB loop. If a module handles the deletion of a key in db 0 and reacts by creating new keys in db 1, the number of keys collected when iterating db 1 could exceed the original global num_keys, causing a buffer overflow at keys_to_del[keys_count++].

Use dynamic reallocation to safely build the snapshot regardless of mid-flight insertions.

🐛 Proposed fix
-        robj **keys_to_del = zmalloc(sizeof(robj*) * num_keys);
+        unsigned int capacity = num_keys > 0 ? num_keys : 16;
+        robj **keys_to_del = zmalloc(sizeof(robj*) * capacity);
         unsigned int keys_count = 0;
 
         kvstoreHashtableIterator *kvs_di = kvstoreGetHashtableIterator(db->keys, hashslot, HASHTABLE_ITER_SAFE);
         void *next;
         while (kvstoreHashtableIteratorNext(kvs_di, &next)) {
+            if (keys_count == capacity) {
+                capacity *= 2;
+                keys_to_del = zrealloc(keys_to_del, sizeof(robj*) * capacity);
+            }
             sds sdskey = objectGetKey((robj*)next);
             keys_to_del[keys_count++] = createStringObject(sdskey, sdslen(sdskey));
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
robj **keys_to_del = zmalloc(sizeof(robj*) * num_keys);
unsigned int keys_count = 0;
kvstoreHashtableIterator *kvs_di = kvstoreGetHashtableIterator(db->keys, hashslot, HASHTABLE_ITER_SAFE);
void *next;
while (kvstoreHashtableIteratorNext(kvs_di, &next)) {
robj *valkey = next;
sds sdskey = objectGetKey((robj*)next);
keys_to_del[keys_count++] = createStringObject(sdskey, sdslen(sdskey));
}
kvstoreReleaseHashtableIterator(kvs_di);
unsigned int capacity = num_keys > 0 ? num_keys : 16;
robj **keys_to_del = zmalloc(sizeof(robj*) * capacity);
unsigned int keys_count = 0;
kvstoreHashtableIterator *kvs_di = kvstoreGetHashtableIterator(db->keys, hashslot, HASHTABLE_ITER_SAFE);
void *next;
while (kvstoreHashtableIteratorNext(kvs_di, &next)) {
if (keys_count == capacity) {
capacity *= 2;
keys_to_del = zrealloc(keys_to_del, sizeof(robj*) * capacity);
}
sds sdskey = objectGetKey((robj*)next);
keys_to_del[keys_count++] = createStringObject(sdskey, sdslen(sdskey));
}
kvstoreReleaseHashtableIterator(kvs_di);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cluster_legacy.c` around lines 7559 - 7568, Update the keys_to_del
snapshot construction in the kvstoreHashtableIterator loop to grow its
allocation dynamically as keys_count approaches capacity, reallocating before
writing each new object. Preserve the existing key-copying and iterator-release
behavior while ensuring insertions during multi-DB processing cannot write
beyond the buffer.

Comment on lines +619 to +637
set found_flushslot 0
set found_individual_dels 0
set empty_count 0
while {$empty_count < 50} {
set cmd [read_from_replication_stream $repl]
if {$cmd eq ""} {
incr empty_count
after 100
continue
}
set empty_count 0
if {[string match -nocase "*flushslot*" $cmd]} {
set found_flushslot 1
break
}
if {[string match -nocase "*unlink*" $cmd] || [string match -nocase "*del*" $cmd]} {
set found_individual_dels 1
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the early break to correctly verify the absence of DEL/UNLINK commands.

Since CLUSTER FLUSHSLOT is propagated before the key deletion loop, breaking the loop immediately upon finding flushslot skips reading the remainder of the replication stream. This renders the test ineffective because found_individual_dels will always remain 0, even if the suppression logic fails and DEL/UNLINK commands are erroneously emitted.

Let the loop consume the entire stream until the timeout is reached (empty_count reaches 50) to guarantee no subsequent deletes are present.

🐛 Proposed fix
         while {$empty_count < 50} {
             set cmd [read_from_replication_stream $repl]
             if {$cmd eq ""} {
                 incr empty_count
                 after 100
                 continue
             }
             set empty_count 0
             if {[string match -nocase "*flushslot*" $cmd]} {
                 set found_flushslot 1
-                break
             }
             if {[string match -nocase "*unlink*" $cmd] || [string match -nocase "*del*" $cmd]} {
                 set found_individual_dels 1
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
set found_flushslot 0
set found_individual_dels 0
set empty_count 0
while {$empty_count < 50} {
set cmd [read_from_replication_stream $repl]
if {$cmd eq ""} {
incr empty_count
after 100
continue
}
set empty_count 0
if {[string match -nocase "*flushslot*" $cmd]} {
set found_flushslot 1
break
}
if {[string match -nocase "*unlink*" $cmd] || [string match -nocase "*del*" $cmd]} {
set found_individual_dels 1
}
}
set found_flushslot 0
set found_individual_dels 0
set empty_count 0
while {$empty_count < 50} {
set cmd [read_from_replication_stream $repl]
if {$cmd eq ""} {
incr empty_count
after 100
continue
}
set empty_count 0
if {[string match -nocase "*flushslot*" $cmd]} {
set found_flushslot 1
}
if {[string match -nocase "*unlink*" $cmd] || [string match -nocase "*del*" $cmd]} {
set found_individual_dels 1
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/cluster/cluster-migrateslots.tcl` around lines 619 - 637, Remove
the early break from the replication-stream loop that checks found_flushslot and
found_individual_dels. After detecting a FLUSHSLOT command, continue reading
until empty_count reaches 50 so subsequent DEL or UNLINK commands are still
detected.

@omanges
omanges force-pushed the asm_opti branch 3 times, most recently from 6ca3060 to e56c34a Compare July 21, 2026 13:15
Signed-off-by: Omkar Mestry <om.m.mestry@gmail.com>
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