Configurable DB hash seed for SCAN family commands consistency (#2608)

Introduce a new config `hash-seed` which can be set only at startup and
controls the hash seed for the server. This includes all hash tables.
This change makes it so that both primaries and replicas will return the
same results for SCAN/HSCAN/ZSCAN/SSCAN cursors. This is useful in order
to make sure SCAN behaves correctly after a failover.

Resolves #4

---------

Signed-off-by: Sarthak Aggarwal <sarthagg@amazon.com>
Signed-off-by: Sarthak Aggarwal <sarthakaggarwal97@gmail.com>
Co-authored-by: Viktor Söderqvist <viktor.soderqvist@est.tech>
This commit is contained in:
Sarthak Aggarwal 2025-11-05 08:45:52 -08:00 committed by GitHub
parent c88c94e326
commit 32844b8b0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 109 additions and 1 deletions

View File

@ -3187,6 +3187,15 @@ static int applyClientMaxMemoryUsage(const char **err) {
return 1; return 1;
} }
#define HASH_SEED_MAX_LEN 256
static int isValidDbHashSeed(sds val, const char **err) {
if (sdslen(val) > HASH_SEED_MAX_LEN) {
*err = "hash-seed must be less than or equal to " STRINGIFY(HASH_SEED_MAX_LEN) " characters";
return 0;
}
return 1;
}
standardConfig static_configs[] = { standardConfig static_configs[] = {
/* Bool configs */ /* Bool configs */
createBoolConfig("rdbchecksum", NULL, IMMUTABLE_CONFIG, server.rdb_checksum, 1, NULL, NULL), createBoolConfig("rdbchecksum", NULL, IMMUTABLE_CONFIG, server.rdb_checksum, 1, NULL, NULL),
@ -3277,6 +3286,7 @@ standardConfig static_configs[] = {
createSDSConfig("primaryauth", "masterauth", MODIFIABLE_CONFIG | SENSITIVE_CONFIG, EMPTY_STRING_IS_NULL, server.primary_auth, NULL, NULL, NULL), createSDSConfig("primaryauth", "masterauth", MODIFIABLE_CONFIG | SENSITIVE_CONFIG, EMPTY_STRING_IS_NULL, server.primary_auth, NULL, NULL, NULL),
createSDSConfig("requirepass", NULL, MODIFIABLE_CONFIG | SENSITIVE_CONFIG, EMPTY_STRING_IS_NULL, server.requirepass, NULL, NULL, updateRequirePass), createSDSConfig("requirepass", NULL, MODIFIABLE_CONFIG | SENSITIVE_CONFIG, EMPTY_STRING_IS_NULL, server.requirepass, NULL, NULL, updateRequirePass),
createSDSConfig("availability-zone", NULL, MODIFIABLE_CONFIG, ALLOW_EMPTY_STRING, server.availability_zone, "", NULL, NULL), createSDSConfig("availability-zone", NULL, MODIFIABLE_CONFIG, ALLOW_EMPTY_STRING, server.availability_zone, "", NULL, NULL),
createSDSConfig("hash-seed", NULL, IMMUTABLE_CONFIG, EMPTY_STRING_IS_NULL, server.hash_seed, NULL, isValidDbHashSeed, NULL),
/* Enum Configs */ /* Enum Configs */
createEnumConfig("supervised", NULL, IMMUTABLE_CONFIG, supervised_mode_enum, server.supervised_mode, SUPERVISED_NONE, NULL, NULL), createEnumConfig("supervised", NULL, IMMUTABLE_CONFIG, supervised_mode_enum, server.supervised_mode, SUPERVISED_NONE, NULL, NULL),

View File

@ -7373,8 +7373,13 @@ __attribute__((weak)) int main(int argc, char **argv) {
sdsfree(options); sdsfree(options);
} }
if (server.sentinel_mode) sentinelCheckConfigFile(); if (server.sentinel_mode) sentinelCheckConfigFile();
if (server.hash_seed != NULL) {
memset(hashseed, 0, sizeof(hashseed));
getHashSeedFromString(hashseed, sizeof(hashseed), server.hash_seed);
hashtableSetHashFunctionSeed(hashseed);
}
/* Do system checks */ /* Do system checks */
#ifdef __linux__ #ifdef __linux__
linuxMemoryWarnings(); linuxMemoryWarnings();
sds err_msg = NULL; sds err_msg = NULL;

View File

@ -2214,6 +2214,7 @@ struct valkeyServer {
* dropping packets of a specific type */ * dropping packets of a specific type */
unsigned long cluster_blacklist_ttl; /* Duration in seconds that a node is denied re-entry into unsigned long cluster_blacklist_ttl; /* Duration in seconds that a node is denied re-entry into
* the cluster after it is forgotten with CLUSTER FORGET. */ * the cluster after it is forgotten with CLUSTER FORGET. */
sds hash_seed; /* Configurable DB hash seed */
int cluster_slot_stats_enabled; /* Cluster slot usage statistics tracking enabled. */ int cluster_slot_stats_enabled; /* Cluster slot usage statistics tracking enabled. */
mstime_t cluster_mf_timeout; /* Milliseconds to do a manual failover. */ mstime_t cluster_mf_timeout; /* Milliseconds to do a manual failover. */
unsigned long cluster_slot_migration_log_max_len; /* Maximum count of migrations to display in the unsigned long cluster_slot_migration_log_max_len; /* Maximum count of migrations to display in the

View File

@ -1037,6 +1037,21 @@ err:
return 0; return 0;
} }
/* Populate the provided seed array by hashing the provided string with SHA256
* and copying the first outlen bytes of the digest into the seed buffer. */
void getHashSeedFromString(unsigned char *seed_array, size_t outlen, const char *value) {
SHA256_CTX ctx;
unsigned char digest[SHA256_BLOCK_SIZE];
sha256_init(&ctx);
sha256_update(&ctx, (const BYTE *)value, strlen(value));
sha256_final(&ctx, digest);
if (outlen > SHA256_BLOCK_SIZE) outlen = SHA256_BLOCK_SIZE;
memcpy(seed_array, digest, outlen);
}
/* Parses a version string on the form "major.minor.patch" and returns an /* Parses a version string on the form "major.minor.patch" and returns an
* integer on the form 0xMMmmpp. Returns -1 on parse error. */ * integer on the form 0xMMmmpp. Returns -1 on parse error. */
int version2num(const char *version) { int version2num(const char *version) {

View File

@ -91,6 +91,7 @@ int trimDoubleString(char *buf, size_t len);
int d2string(char *buf, size_t len, double value); int d2string(char *buf, size_t len, double value);
int fixedpoint_d2string(char *dst, size_t dstlen, double dvalue, int fractional_digits); int fixedpoint_d2string(char *dst, size_t dstlen, double dvalue, int fractional_digits);
int ld2string(char *buf, size_t len, long double value, ld2string_mode mode); int ld2string(char *buf, size_t len, long double value, ld2string_mode mode);
void getHashSeedFromString(unsigned char *seed_array, size_t len, const char *value);
int double2ll(double d, long long *out); int double2ll(double d, long long *out);
int version2num(const char *version); int version2num(const char *version);
int yesnotoi(char *s); int yesnotoi(char *s);

View File

@ -0,0 +1,60 @@
test {scan family consistency with configured hash seed} {
start_server {tags {"external:skip"}} {
set fixed_seed "aabbccddeeffgghh"
set shared_overrides [list appendonly no save "" hash-seed $fixed_seed activedefrag no hz 1]
start_server [list overrides $shared_overrides] {
set primary_host [srv 0 host]
set primary_port [srv 0 port]
start_server [list overrides $shared_overrides] {
set primary [srv -1 client]
set replica [srv 0 client]
$primary flushall
$replica replicaof $primary_host $primary_port
wait_for_sync $replica
set n 50
for {set i 0} {$i < $n} {incr i} {
$primary set "k:$i" x
$primary hset h "f:$i" $i
$primary sadd s "m:$i"
$primary zadd z $i "m:$i"
}
wait_for_condition 200 50 {
[$replica dbsize] == [$primary dbsize]
} else {
fail "replica did not catch up dbsize (primary=[$primary dbsize], replica=[$replica dbsize])"
}
set cursor {{0} {}}
while {1} {
set primary_cursor_next [$primary scan [lindex $cursor 0]]
set replica_cursor_next [$replica scan [lindex $cursor 0]]
assert_equal $primary_cursor_next $replica_cursor_next
if {[lindex $primary_cursor_next 0] eq "0"} {
assert_equal "0" [lindex $replica_cursor_next 0]
break
}
set cursor $primary_cursor_next
}
foreach {cmd key} {hscan h sscan s zscan z} {
set cursor {{0} {}}
while {1} {
set primary_cursor_next [$primary $cmd $key [lindex $cursor 0]]
set replica_cursor_next [$replica $cmd $key [lindex $cursor 0]]
assert_equal $primary_cursor_next $replica_cursor_next
if {[lindex $primary_cursor_next 0] eq "0"} {
assert_equal "0" [lindex $replica_cursor_next 0]
break
}
set cursor $primary_cursor_next
}
}
}
}
}
}

View File

@ -1225,6 +1225,7 @@ start_server {tags {"introspection"}} {
disable-thp disable-thp
aclfile aclfile
unixsocket unixsocket
hash-seed
pidfile pidfile
syslog-ident syslog-ident
appendfilename appendfilename
@ -1956,3 +1957,11 @@ test {CONFIG REWRITE handles alias config properly} {
assert_equal [r config get hash-max-listpack-entries] {hash-max-listpack-entries 100} assert_equal [r config get hash-max-listpack-entries] {hash-max-listpack-entries 100}
} }
} {} {external:skip} } {} {external:skip}
test {CONFIG hash-seed is immutable and settable at startup} {
start_server {tags {"introspection"} overrides {hash-seed aabbccddeeffgghh}} {
assert_error "ERR CONFIG SET failed (possibly related to argument 'hash-seed') - can't set immutable config*" {
r config set hash-seed newseed
}
}
} {} {external:skip}

View File

@ -527,6 +527,13 @@ locale-collate ""
# #
# availability-zone "zone-name" # availability-zone "zone-name"
# Use a fixed hash seed for hashtable instead of a random one.
# Setting this option makes commands like SCAN return keys in a consistent
# order across restarts and failovers. The seed can be any string up to 256 characters.
# The value is immutable and must be provided only at server startup.
#
# hash-seed example-seed-val
################################ SNAPSHOTTING ################################ ################################ SNAPSHOTTING ################################
# Save the DB to disk. # Save the DB to disk.