Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/networking.c
Original file line number Diff line number Diff line change
Expand Up @@ -4301,6 +4301,7 @@ sds catClientInfoString(sds s, client *client, int hide_user_data) {

p = capa;
if (client->capa & CLIENT_CAPA_REDIRECT) *p++ = 'r';
if (client->capa & CLIENT_CAPA_REDIRECT_KEYLESS) *p++ = 'k';
*p = '\0';

/* Compute the total memory consumed by this client. */
Expand Down Expand Up @@ -4782,6 +4783,7 @@ static int validateClientCapaFilter(sds capa) {
const char capability = capa[i];
switch (capability) {
case 'r':
case 'k':
/* Valid capability, do nothing. */
break;
default:
Expand Down Expand Up @@ -4898,6 +4900,9 @@ static int clientMatchesCapaFilter(client *c, sds capa_filter) {
case 'r': /* client supports redirection */
if (!(c->capa & CLIENT_CAPA_REDIRECT)) return 0;
break;
case 'k': /* client supports keyless redirection */
if (!(c->capa & CLIENT_CAPA_REDIRECT_KEYLESS)) return 0;
break;
default:
/* Invalid capa, return false */
return 0;
Expand Down Expand Up @@ -5005,6 +5010,8 @@ void clientHelpCommand(client *c) {
" The client claims its some capability options. Options are:",
" * REDIRECT",
" The client can handle redirection during primary and replica failover in standalone mode.",
" * REDIRECT-KEYLESS",
" Redirect keyless read commands to the primary when the replica is not in READONLY mode.",
"GETREDIR",
" Return the client ID we are redirecting to when tracking is enabled.",
"GETNAME",
Expand Down Expand Up @@ -5610,6 +5617,8 @@ void clientCapaCommand(client *c) {
for (int i = 2; i < c->argc; i++) {
if (!strcasecmp(objectGetVal(c->argv[i]), "redirect")) {
c->capa |= CLIENT_CAPA_REDIRECT;
} else if (!strcasecmp(objectGetVal(c->argv[i]), "redirect-keyless")) {
c->capa |= CLIENT_CAPA_REDIRECT_KEYLESS;
}
}
addReply(c, shared.ok);
Expand Down
28 changes: 26 additions & 2 deletions src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -4409,8 +4409,8 @@ int processCommand(client *c) {
* However we don't perform the redirection if:
* 1) The sender of this command is our primary.
* 2) The command has no key arguments. */
if (server.cluster_enabled && !obey_client &&
!(!(c->cmd->flags & CMD_MOVABLE_KEYS) && c->cmd->key_specs_num == 0 && c->cmd->proc != execCommand)) {
int is_keyless = !(c->cmd->flags & CMD_MOVABLE_KEYS) && c->cmd->key_specs_num == 0 && c->cmd->proc != execCommand;
if (server.cluster_enabled && !obey_client && !is_keyless) {
int error_code;
clusterNode *n = getNodeByQuery(c, &error_code);
if (n == NULL || !clusterNodeIsMyself(n)) {
Expand All @@ -4426,6 +4426,30 @@ int processCommand(client *c) {
}
}

/* If the client has the redirect-keyless capability, redirect keyless
* read commands to the primary when this is a replica and the client
* has not opted into replica reads with READONLY. EXEC with all-keyless
* queued commands is also considered keyless (c->slot remains -1 after
* getNodeByQuery). */
int is_keyless_exec = is_exec && c->slot == -1;
if (server.cluster_enabled && !obey_client && (is_keyless || is_keyless_exec) && is_read_command &&
(c->capa & CLIENT_CAPA_REDIRECT_KEYLESS) && !c->flag.readonly) {
clusterNode *myself = getMyClusterNode();
if (clusterNodeIsReplica(myself)) {
clusterNode *primary = clusterNodeGetPrimary(myself);
if (is_keyless_exec) {
discardTransaction(c);
} else {
flagTransaction(c);
}
/* We pass -1 as it does not matter which slot will be passed in MOVED command */
clusterRedirectClient(c, primary, -1, CLUSTER_REDIR_MOVED);
c->duration = 0;
c->cmd->rejected_calls++;
return C_OK;
}
}

if (clientSupportStandAloneRedirect(c) && !obey_client &&
(is_write_command || (is_read_command && !c->flag.readonly))) {
if (server.failover_state == FAILOVER_IN_PROGRESS) {
Expand Down
3 changes: 2 additions & 1 deletion src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,8 @@ extern int configOOMScoreAdjValuesDefaults[CONFIG_OOM_COUNT];
#define CMD_DOC_SYSCMD (1 << 1) /* System (internal) command */

/* Client capabilities */
#define CLIENT_CAPA_REDIRECT (1 << 0) /* Indicate that the client can handle redirection */
#define CLIENT_CAPA_REDIRECT (1 << 0) /* Indicate that the client can handle redirection */
#define CLIENT_CAPA_REDIRECT_KEYLESS (1 << 1) /* Redirect keyless read commands to primary in cluster mode */

/* Client block type (btype field in client structure)
* if CLIENT_BLOCKED flag is set. */
Expand Down
108 changes: 108 additions & 0 deletions tests/unit/cluster/replica-redirect.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,112 @@ start_cluster 1 1 {tags {external:skip cluster}} {
$rd0_ro close
$rd1 close
}

test {keyless read commands execute on replica without redirect-keyless capa} {
# Without the capa, keyless read commands like SCAN execute locally on the replica
# After failover, node 0 is the new replica.
set rd [valkey_deferring_client 0]
$rd SCAN 0
set reply [$rd read]
assert_match "0 *" $reply
$rd close
}

test {keyless read commands are MOVED with redirect-keyless capa} {
set rd [valkey_deferring_client 0]
$rd CLIENT CAPA redirect-keyless
assert_equal OK [$rd read]

$rd DBSIZE
assert_error "MOVED *" {$rd read}

$rd RANDOMKEY
assert_error "MOVED *" {$rd read}

$rd SCAN 0
assert_error "MOVED *" {$rd read}

$rd close
}

test {keyless read commands execute on replica with redirect-keyless and READONLY} {
set rd [valkey_deferring_client 0]
$rd CLIENT CAPA redirect-keyless
assert_equal OK [$rd read]
$rd READONLY
assert_equal OK [$rd read]

# With READONLY, keyless reads should execute locally
$rd DBSIZE
set reply [$rd read]
assert {$reply >= 0}

$rd close
}

test {non-read keyless commands are not affected by redirect-keyless capa} {
set rd [valkey_deferring_client 0]
$rd CLIENT CAPA redirect-keyless
assert_equal OK [$rd read]

# PING is not CMD_READONLY, should still work on replica
$rd PING
assert_equal PONG [$rd read]

$rd close
}

test {CLIENT INFO reports redirect-keyless capa} {
set rd [valkey_deferring_client 0]
$rd CLIENT CAPA redirect-keyless
assert_equal OK [$rd read]

$rd CLIENT INFO
assert_match "*capa=k*" [$rd read]

$rd close
}

test {keyless commands inside MULTI are individually MOVED with redirect-keyless capa} {
set rd [valkey_deferring_client 0]
$rd CLIENT CAPA redirect-keyless
assert_equal OK [$rd read]

$rd MULTI
assert_equal OK [$rd read]
# Individual keyless commands get MOVED, consistent with keyed commands
$rd DBSIZE
assert_error "MOVED -1 *" {$rd read}
$rd RANDOMKEY
assert_error "MOVED -1 *" {$rd read}
# Transaction was flagged dirty, EXEC returns EXECABORT
$rd EXEC
assert_error "EXECABORT *" {$rd read}

$rd PING
assert_equal PONG [$rd read]

$rd close
}

test {both redirect and redirect-keyless capas work together} {
set rd [valkey_deferring_client 0]
$rd CLIENT CAPA redirect
assert_equal OK [$rd read]
$rd CLIENT CAPA redirect-keyless
assert_equal OK [$rd read]

$rd CLIENT INFO
assert_match "*capa=rk*" [$rd read]

# Keyless read is redirected via redirect-keyless
$rd DBSIZE
assert_error "MOVED -1 *" {$rd read}

# Keyed read is redirected via standard cluster redirect
$rd GET x
assert_error "MOVED *" {$rd read}

$rd close
}
} ;# start_cluster
23 changes: 21 additions & 2 deletions tests/unit/introspection.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,19 @@ start_server {tags {"introspection"}} {
$c1 client setname "client-with-r"
$c1 client capa redirect

set c2 [valkey_client]
$c2 client setname "client-with-k"
$c2 client capa redirect-keyless

set output [r client list capa r capa r]
assert_match *client-with-r* $output

set output [r client list capa k]
assert_match *client-with-k* $output
assert_no_match *client-with-r* $output

catch {$c1 close}
catch {$c2 close}
}

test {CLIENT KILL with IP filter} {
Expand Down Expand Up @@ -285,10 +295,19 @@ start_server {tags {"introspection"}} {
$c1 client setname "killme-capa"
$c1 client capa redirect

# Kill using capa filter
r client kill capa r skipme yes
set c2 [valkey_client]
$c2 client setname "killme-capa-k"
$c2 client capa redirect-keyless

# Kill using capa r filter
r client kill capa r skipme yes
assert_error "*I/O error*" {$c1 ping}
# c2 should still be alive (only has k, not r)
assert_equal {PONG} [$c2 ping]

# Now kill using capa k filter
r client kill capa k skipme yes
assert_error "*I/O error*" {$c2 ping}
} {}

test {CLIENT KILL with NAME filter} {
Expand Down
Loading