diff --git a/src/config.c b/src/config.c index 9ea28298d7..8261fa02e3 100644 --- a/src/config.c +++ b/src/config.c @@ -3210,6 +3210,7 @@ standardConfig static_configs[] = { createBoolConfig("cluster-slot-stats-enabled", NULL, MODIFIABLE_CONFIG, server.cluster_slot_stats_enabled, 0, NULL, NULL), createBoolConfig("hide-user-data-from-log", NULL, MODIFIABLE_CONFIG, server.hide_user_data_from_log, 1, NULL, NULL), createBoolConfig("import-mode", NULL, DEBUG_CONFIG | MODIFIABLE_CONFIG, server.import_mode, 0, NULL, NULL), + createBoolConfig("allow-empty-set", NULL, MODIFIABLE_CONFIG, server.allow_empty_set, 0, NULL, NULL), /* String Configs */ createStringConfig("aclfile", NULL, IMMUTABLE_CONFIG, ALLOW_EMPTY_STRING, server.acl_filename, "", NULL, NULL), @@ -3344,7 +3345,7 @@ standardConfig static_configs[] = { /* Size_t configs */ createSizeTConfig("hash-max-listpack-entries", "hash-max-ziplist-entries", MODIFIABLE_CONFIG, 0, LONG_MAX, server.hash_max_listpack_entries, 512, INTEGER_CONFIG, NULL, NULL), - createSizeTConfig("set-max-intset-entries", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.set_max_intset_entries, 512, INTEGER_CONFIG, NULL, NULL), + createSizeTConfig("set-max-intset-entries", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.set_max_intset_entries, 2, INTEGER_CONFIG, NULL, NULL), createSizeTConfig("set-max-listpack-entries", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.set_max_listpack_entries, 128, INTEGER_CONFIG, NULL, NULL), createSizeTConfig("set-max-listpack-value", NULL, MODIFIABLE_CONFIG, 0, LONG_MAX, server.set_max_listpack_value, 64, INTEGER_CONFIG, NULL, NULL), createSizeTConfig("zset-max-listpack-entries", "zset-max-ziplist-entries", MODIFIABLE_CONFIG, 0, LONG_MAX, server.zset_max_listpack_entries, 128, INTEGER_CONFIG, NULL, NULL), diff --git a/src/intset.c b/src/intset.c index ddddae221d..6258f5b195 100644 --- a/src/intset.c +++ b/src/intset.c @@ -320,9 +320,6 @@ int intsetValidateIntegrity(const unsigned char *p, size_t size, int deep) { uint32_t count = intrev32ifbe(is->length); if (sizeof(*is) + count * record_size != size) return 0; - /* check that the set is not empty. */ - if (count == 0) return 0; - if (!deep) return 1; /* check that there are no dup or out of order records. */ diff --git a/src/rdb.c b/src/rdb.c index ca904f7f98..27b6bbe5e4 100644 --- a/src/rdb.c +++ b/src/rdb.c @@ -1899,7 +1899,7 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { } else if (rdbtype == RDB_TYPE_SET) { /* Read Set value */ if ((len = rdbLoadLen(rdb, NULL)) == RDB_LENERR) return NULL; - if (len == 0) goto emptykey; + if (!server.allow_empty_set && len == 0) goto emptykey; /* Use a regular set when there are too many entries. */ size_t max_entries = server.set_max_intset_entries; @@ -2346,6 +2346,12 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { } case RDB_TYPE_SET_INTSET: if (deep_integrity_validation) server.stat_dump_payload_sanitizations++; + if (!server.allow_empty_set && intsetLen((intset *)encoded) == 0) { + zfree(encoded); + o->ptr = NULL; + decrRefCount(o); + goto emptykey; + } if (!intsetValidateIntegrity(encoded, encoded_len, deep_integrity_validation)) { rdbReportCorruptRDB("Intset integrity check failed."); zfree(encoded); @@ -2370,10 +2376,14 @@ robj *rdbLoadObject(int rdbtype, rio *rdb, sds key, int dbid, int *error) { o->encoding = OBJ_ENCODING_LISTPACK; if (setTypeSize(o) == 0) { - zfree(encoded); - o->ptr = NULL; - decrRefCount(o); - goto emptykey; + if (!server.allow_empty_set) { + zfree(encoded); + o->ptr = NULL; + decrRefCount(o); + goto emptykey; + } + + o->ptr = lpNew(0); } if (setTypeSize(o) > server.set_max_listpack_entries) setTypeConvert(o, OBJ_ENCODING_HT); break; diff --git a/src/server.h b/src/server.h index 44de6eada1..af69d14bc8 100644 --- a/src/server.h +++ b/src/server.h @@ -1925,6 +1925,8 @@ struct valkeyServer { invocation of the event loop. */ unsigned int max_new_conns_per_cycle; /* The maximum number of tcp connections that will be accepted during each invocation of the event loop. */ + int allow_empty_set; + /* AOF persistence */ int aof_enabled; /* AOF configuration */ int aof_state; /* AOF_(ON|OFF|WAIT_REWRITE) */ diff --git a/src/t_set.c b/src/t_set.c index a540c3c49b..bc45dd412c 100644 --- a/src/t_set.c +++ b/src/t_set.c @@ -621,8 +621,10 @@ void sremCommand(client *c) { if (setTypeRemove(set, c->argv[j]->ptr)) { deleted++; if (setTypeSize(set) == 0) { - dbDelete(c->db, c->argv[1]); - keyremoved = 1; + if (!server.allow_empty_set) { + dbDelete(c->db, c->argv[1]); + keyremoved = 1; + } break; } } @@ -666,7 +668,7 @@ void smoveCommand(client *c) { notifyKeyspaceEvent(NOTIFY_SET, "srem", c->argv[1], c->db->id); /* Remove the src set from the database when empty */ - if (setTypeSize(srcset) == 0) { + if (!server.allow_empty_set && setTypeSize(srcset) == 0) { dbDelete(c->db, c->argv[1]); notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id); } @@ -969,7 +971,7 @@ void spopCommand(client *c) { decrRefCount(ele); /* Delete the set if it's empty */ - if (setTypeSize(set) == 0) { + if (!server.allow_empty_set && setTypeSize(set) == 0) { dbDelete(c->db, c->argv[1]); notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id); } diff --git a/tests/unit/type/set.tcl b/tests/unit/type/set.tcl index 944c3d3d98..79259726d7 100644 --- a/tests/unit/type/set.tcl +++ b/tests/unit/type/set.tcl @@ -1271,6 +1271,37 @@ foreach type {single multiple single_multiple} { } } +start_server { + tags {"set"} + overrides { + "set-max-intset-entries" 1 + "allow-empty-set" "yes" + } +} { + proc save_load_rdb {} { + r save + restart_server 0 true false + wait_done_loading r + } + test {SADD save and reload empty listset from RDB} { + r flushdb + assert_equal 1 [r sadd listset a] + assert_equal 1 [r srem listset a] + assert_equal 0 [r scard listset] + assert_equal 1 [r exists listset] + save_load_rdb + assert_equal 0 [r scard listset] + assert_equal 1 [r exists listset] + } + # intset + # hashset + # smove + # spop + # sunion + # skip empty sets when loading rdb & not allow empty set +} + + run_solo {set-large-memory} { start_server [list overrides [list save ""] ] { diff --git a/valkey.conf b/valkey.conf index e23aea39de..8238ff001e 100644 --- a/valkey.conf +++ b/valkey.conf @@ -496,6 +496,9 @@ locale-collate "" # # extended-redis-compatibility no +# description for empty set +allow-empty-set yes + ################################ SNAPSHOTTING ################################ # Save the DB to disk.