Skip to content

Conversation

@murphyjacob4
Copy link
Contributor

@murphyjacob4 murphyjacob4 commented Jun 3, 2025

Includes three changes:

  1. Route CLUSTER FLUSHSLOT based on the target slot: Introduces a USES_SLOT flag to key_specs that allows for key_specs to denote target slots instead of individual keys. This allows CLUSTER FLUSHSLOT to return -MOVED for unowned slots, and will help with determining slot migrations to propagate to in Introduce atomic slot migration #1949
  2. Propagate as CLUSTER FLUSHSLOT: When we execute delKeysInSlot after a cluster topology update that results in a primary losing some slots, this will now propagate as as single CLUSTER FLUSHSLOT rather than an UNLINK command for each key in the slot. This will be useful for Introduce atomic slot migration #1949 when a migration is completed.

I also attempted a change to delete the slot hashtable instead of iterating over each key in the hashtable and deleting one-by-one. However, it turns out there are events that are triggered by unlinking a specific key that would require iteration, accessing the key's value, and triggering. Overall this probably won't give much of an improvement unless we can figure out a better story for triggering keyspace notification, so not including in this PR, but noting for posterity.

@codecov
Copy link

codecov bot commented Jun 3, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 71.48%. Comparing base (3789b29) to head (83a6c63).

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #2167      +/-   ##
============================================
+ Coverage     71.43%   71.48%   +0.04%     
============================================
  Files           122      122              
  Lines         66210    66244      +34     
============================================
+ Hits          47300    47352      +52     
+ Misses        18910    18892      -18     
Files with missing lines Coverage Δ
src/cluster.c 90.48% <100.00%> (+0.10%) ⬆️
src/cluster_legacy.c 86.80% <100.00%> (+0.08%) ⬆️
src/commands.def 100.00% <ø> (ø)
src/db.c 90.07% <100.00%> (ø)
src/server.c 87.91% <100.00%> (-0.02%) ⬇️
src/server.h 100.00% <ø> (ø)

... and 11 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.

@murphyjacob4
Copy link
Contributor Author

Hmm, looks like this breaks the module API since it deletes the keys from the dictionary after the keyspace event, will add a commit to fix this

@murphyjacob4 murphyjacob4 changed the title Improve CLUSTER FLUSHSLOT routing, deletion, and propagation Improve CLUSTER FLUSHSLOT routing and propagation Jun 4, 2025
@murphyjacob4
Copy link
Contributor Author

Removed the code from this PR that supported drop the slot hashtable from the DB instead of unlinking each key directly. The issue is that there are some special module events (moduleNotifyKeyUnlink) that we would still need to iterate and trigger for each key.

Ideally we can align FLUSHSLOT with FLUSHDB for module events - and have modules handle slot flush as a separate event rather than requiring each key to be broadcasted individually. But this would be a breaking change

Signed-off-by: Jacob Murphy <[email protected]>
@madolson
Copy link
Member

madolson commented Jun 4, 2025

Ideally we can align FLUSHSLOT with FLUSHDB for module events - and have modules handle slot flush as a separate event rather than requiring each key to be broadcasted individually. But this would be a breaking change

We could make a breaking change for atomic slot migration and modules in 9.0.

Copy link
Member

@madolson madolson left a comment

Choose a reason for hiding this comment

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

It's a bit weird that only this command will do redirects, but it seems like a step in the right direction.

enterExecutionUnit(1, 0);

/* Propagate as a single CLUSTER FLUSHSLOT <slot> ASYNC/SYNC. */
if (propagate_del) {
Copy link
Member

Choose a reason for hiding this comment

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

Do the hooks fire if the replica executes flushslot? Couldn't the module hooks also need to get executed on the replica? I would imagine something like search might break.

Copy link
Member

Choose a reason for hiding this comment

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

The replica might also not support CLUSTER FLUSHSLOT, we should check the version of the replica. Otherwise it's a backwards incompatible change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do the hooks fire if the replica executes flushslot

Yeah - so CLUSTER FLUSHSLOT will trigger delKeysInSlot (this function) with send_del_event == true. send_del_event tells the function to propagate keyspace notifications to clients and modules, whereas if false, it would just propagate to modules.

Is CLUSTER FLUSHSLOT something we are going to let users do? I seem to remember conversation that this would be internal only. If we go the internal-only route - it makes sense to me that we would just fire the module events on CLUSTER FLUSHSLOT (send_del_event == false)

I guess there is a discrepancy where the primary would not fire keyspace events to clients, but the replica would. So we should align those behaviors.

Any objection to making CLUSTER FLUSHSLOT not send keyspace notifications to clients? The only downside is if a user triggers CLUSTER FLUSHSLOT - we should differentiate (probably through an additional argument) since it is a "true deletion" (no longer stored in cluster) vs a "local deletion" (due to migration, but still stored in the cluster). But if we aren't making this an end-user command, I think we can just assume this is from "local deletion"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The replica might also not support CLUSTER FLUSHSLOT, we should check the version of the replica. Otherwise it's a backwards incompatible change.

Good point. I've addressed this now where we only send CLUSTER FLUSHSLOT to replicas if they are all on 9.0.0+

Copy link
Member

Choose a reason for hiding this comment

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

I guess there is a discrepancy where the primary would not fire keyspace events to clients, but the replica would. So we should align those behaviors.

Yeah. I agree that consistency is more important.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess this is the existing behavior, since delKeysInSlot will not generate keyspace events on the primary, but the replicated UNLINK will. Triggering CLUSTER FLUSHSLOT on the replica is the same effect.

@hpatro
Copy link
Collaborator

hpatro commented Jun 4, 2025

Unrelated but feels like a good thread to check about improvement around FLUSHSLOT, Would it be beneficial to introduce FLUSHSLOTSRANGE as well ?

Copy link
Collaborator

@hpatro hpatro left a comment

Choose a reason for hiding this comment

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

mostly nit picks.

argv[1] = shared.flushslot;
argv[2] = createStringObjectFromLongLong(hashslot);
argv[3] = lazy ? shared.async : shared.sync;
alsoPropagate(/*dbid=*/-1, argv, 4, PROPAGATE_AOF | PROPAGATE_REPL);
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit:

Suggested change
alsoPropagate(/*dbid=*/-1, argv, 4, PROPAGATE_AOF | PROPAGATE_REPL);
alsoPropagate(-1, argv, 4, PROPAGATE_AOF | PROPAGATE_REPL);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I guess this is just a Google style thing (https://google.github.io/styleguide/cppguide.html#Function_Argument_Comments). But I'll remove it

return -1;
}

return (int)slot;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we can introduce getIntFromObject helper.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense to me

char *err = NULL;
int slot = getSlotOrError(o, &err);
if (err) {
addReplyErrorSds(c, sdsnew(err));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would this suffice?

Suggested change
addReplyErrorSds(c, sdsnew(err));
addReplyError(c, err);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

addReplyErrorSds(c, sdsnew(err));
return -1;
}
return (int)slot;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return (int)slot;
return slot;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@hwware
Copy link
Member

hwware commented Jun 5, 2025

I just begin going through the code changes, one question for the flag name: USES_SLOT. Why it includes an 'S' instead of use_slot, or using_slot, I am a little bit confused the Singular and plural

@murphyjacob4
Copy link
Contributor Author

one question for the flag name: USES_SLOT. Why it includes an 'S' instead of use_slot, or using_slot

Perhaps USING_SLOT is a little more readable. I was thinking grammatically along the lines of "this key_spec uses a slot instead of a key"

/* Get the slot from robj and return it. If the slot is not valid,
* return -1 and send an error to the client. */
int getSlotOrReply(client *c, robj *o) {
int getSlotOrError(robj *o, char **err_out) {
Copy link
Member

Choose a reason for hiding this comment

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

I think the functions getSlotOrError and getSlotOrReply are overlapped in some logic, I prefre to combine them as getSlotOrReply(client *c, robj o, char message)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How would that work? We would return an error if message is provided, or a reply if client is provided?

To me, they don't overlap in logic, it is just that getSlotOrReply is wrapping getSlotOrError and sending that to the client. No logic is repeated, it is just composed of the other function. Not all the times we want to turn an robj to a slot do we want to require a client response to be involved.

Copy link
Member

Choose a reason for hiding this comment

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

It works like this way?

int getSlotOrReply(client *c, robj *o, char **err_out) {
long long slot;

if (getLongLongFromObject(o, &slot) != C_OK || slot < 0 || slot >= CLUSTER_SLOTS) {
    *err_out = "Invalid or out of range slot";
     addReplyErrorSds(c, sdsnew(err));
     return -1;
}

return (int)slot;

}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the case for this PR we want to parse the slot but not add a reply to the client (since we leave this to the actual command handler). This is why they are two functions (one does not reply to the client)

@madolson
Copy link
Member

Perhaps USING_SLOT is a little more readable. I was thinking grammatically along the lines of "this key_spec uses a slot instead of a key"

I was expecting to be more like, IS_SLOT. It's less that the keyspec is using a slot, than that it's defining where the slot really is.

Copy link
Member

@madolson madolson left a comment

Choose a reason for hiding this comment

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

Mostly minor comments. I'm only like 80% convinced routing the flushslot command based on custom routing is really needed, but I also think it's a major decision since it will require client side changes.

@madolson madolson added major-decision-pending Major decision pending by TSC team release-notes This issue should get a line item in the release notes labels Jun 10, 2025
Signed-off-by: Jacob Murphy <[email protected]>
Signed-off-by: Jacob Murphy <[email protected]>
Signed-off-by: Jacob Murphy <[email protected]>
Signed-off-by: Jacob Murphy <[email protected]>
@murphyjacob4
Copy link
Contributor Author

I was expecting to be more like, IS_SLOT. It's less that the keyspec is using a slot, than that it's defining where the slot really is.

Yeah that naming sounds good to me

Mostly minor comments. I'm only like 80% convinced routing the flushslot command based on custom routing is a good idea, but I also think it's a major decision since it will require client side changes.

Right... from an immediate usability perspective, I guess the other mechanism (any node will accept CLUSTER FLUSHSLOT, but it will be a no-op if it is not owned) could be easier to program against for clients, since the client could just send CLUSTER FLUSHSLOT X to every node. You could even do this today in valkey-py with something like cluster.execute_command("CLUSTER FLUSHSLOT 16383", target=valkey.ALL).

But on the other hand - it does seem unintuitive - if I use valkey-cli on a node and call CLUSTER FLUSHSLOT X and get back +OK - I would kind of expect that the slot is flushed (even if it isn't owned locally).

Since this is the first slot-level write command that we are adding, I feel like we should probably route it as we would a multi-key write command affecting all keys in the slot would be routed. A quick look at the code for valkey-py shows that they already need functionality like this for commands like SETSLOT: https://github.com/valkey-io/valkey-py/blob/main/valkey/asyncio/cluster.py#L628-L630

But yeah - let's discuss in a wider group

@madolson
Copy link
Member

But yeah - let's discuss in a wider group

CLUSTER SCAN might also do the same thing, so it seems like there will be more future commands that need this routing behavior.

@murphyjacob4
Copy link
Contributor Author

Discussed with the wider group. We think that since it is a net new command, their is low likelihood of the new routing breaking client applications. If users want to use it and their client doesn't natively support it, they can send it as a raw command and their client should handle the MOVED redirect

@murphyjacob4
Copy link
Contributor Author

@valkey-io/core-team Please vote 👍/👎

@madolson madolson mentioned this pull request Aug 18, 2025
17 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

major-decision-pending Major decision pending by TSC team release-notes This issue should get a line item in the release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants