This is an automated email from the ASF dual-hosted git repository.
twice pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new 37140d6c3 feat(hash): initialize field expiration metadata and subkey
en/decoding (#3444)
37140d6c3 is described below
commit 37140d6c30be4fa91e8a810e8be83981d618ce12
Author: Twice <[email protected]>
AuthorDate: Tue Apr 14 23:03:38 2026 +0800
feat(hash): initialize field expiration metadata and subkey en/decoding
(#3444)
Part of #3436. See proposal #3432.
This PR includes:
- metadata changes for the new encoding
- subkey encoding/decoding and related logic in `redis_hash.cc`
- new config option adding
- an enhancement to `SubKeyScanner` so that it can be used for HFE
- run hash test cases for both modes
Assisted-by: Codex
---
kvrocks.conf | 20 +++
src/config/config.cc | 15 ++
src/config/config.h | 2 +
src/search/indexer.cc | 10 +-
src/search/indexer.h | 2 +-
src/storage/redis_db.cc | 37 +---
src/storage/redis_db.h | 64 +++++++
src/storage/redis_metadata.cc | 75 ++++++++
src/storage/redis_metadata.h | 30 +++-
src/types/redis_hash.cc | 115 ++++++++----
src/types/redis_hash.h | 5 +-
tests/cppunit/config_test.cc | 2 +
tests/cppunit/metadata_test.cc | 57 ++++++
tests/cppunit/types/hash_test.cc | 104 +++++++++++
tests/gocase/unit/config/config_test.go | 102 +++++++++--
tests/gocase/unit/geo/geo_test.go | 10 +-
tests/gocase/unit/kmetadata/kmetadata_test.go | 5 +-
tests/gocase/unit/pubsub/pubsub_test.go | 10 +-
tests/gocase/unit/scripting/function_test.go | 10 +-
tests/gocase/unit/type/bloom/bloom_test.go | 5 +-
tests/gocase/unit/type/hash/hash_test.go | 193 ++++++++++++---------
tests/gocase/unit/type/incr/incr_test.go | 5 +-
tests/gocase/unit/type/json/json_test.go | 5 +-
tests/gocase/unit/type/list/list_test.go | 25 ++-
tests/gocase/unit/type/set/set_test.go | 10 +-
tests/gocase/unit/type/sint/sint_test.go | 5 +-
tests/gocase/unit/type/stream/stream_test.go | 10 +-
tests/gocase/unit/type/strings/strings_test.go | 5 +-
tests/gocase/unit/type/tdigest/tdigest_test.go | 15 +-
.../gocase/unit/type/timeseries/timeseries_test.go | 5 +-
tests/gocase/unit/type/zset/zset_test.go | 10 +-
tests/gocase/util/configs.go | 52 ++----
32 files changed, 731 insertions(+), 289 deletions(-)
diff --git a/kvrocks.conf b/kvrocks.conf
index 12b019ca1..f1541c496 100644
--- a/kvrocks.conf
+++ b/kvrocks.conf
@@ -77,6 +77,26 @@ repl-namespace-enabled no
#
# proto-max-bulk-len 536870912
+# The encoding mode for newly created hash keys.
+# - legacy: use the original subkey format (no per-field expire support).
+# Existing data written in this mode is always readable regardless of this
setting.
+# - field-expiration: use the new subkey format that prepends an 8-byte expire
+# timestamp to each field value. Required for HEXPIRE/HPERSIST/HPTTL
commands.
+# NOTE: this only affects newly created keys. Existing keys retain their
original mode.
+# Default: legacy
+# Available options: legacy, field-expiration
+hash-encoding-mode legacy
+
+# How HLEN computes the number of fields when some fields have a TTL.
+# - accurate: return the exact count. O(1) when no fields have TTL or none have
+# expired yet; otherwise performs a scan to remove expired fields and update
metadata.
+# - approximate: always return the stored size without scanning. The count may
include
+# fields that have already expired but have not yet been cleaned up.
+# This option has no effect when hash-encoding-mode is legacy.
+# Default: accurate
+# Available options: accurate, approximate
+hash-length-mode accurate
+
# Persist the cluster nodes topology in local file($dir/nodes.conf). This
configuration
# takes effect only if the cluster mode was enabled.
#
diff --git a/src/config/config.cc b/src/config/config.cc
index 416eabd9a..492eb975b 100644
--- a/src/config/config.cc
+++ b/src/config/config.cc
@@ -93,6 +93,16 @@ const std::vector<ConfigEnum<BlockCacheType>> cache_types{[]
{
const std::vector<ConfigEnum<MigrationType>> migration_types{{"redis-command",
MigrationType::kRedisCommand},
{"raw-key-value",
MigrationType::kRawKeyValue}};
+const std::vector<ConfigEnum<HashSubkeyEncodingMode>>
hash_subkey_encoding_modes{
+ {"legacy", HashSubkeyEncodingMode::kLegacy},
+ {"field-expiration", HashSubkeyEncodingMode::kFieldExpiration},
+};
+
+const std::vector<ConfigEnum<HashLengthMode>> hash_length_modes{
+ {"accurate", HashLengthMode::kAccurate},
+ {"approximate", HashLengthMode::kApproximate},
+};
+
std::string TrimRocksDbPrefix(std::string s) {
constexpr std::string_view prefix = "rocksdb.";
if (!util::StartsWithICase(s, prefix)) return s;
@@ -237,6 +247,11 @@ Config::Config() {
{"redis-cursor-compatible", false, new
YesNoField(&redis_cursor_compatible, true)},
{"redis-databases", true, new IntField(&redis_databases, 0, 0, INT_MAX)},
{"resp3-enabled", false, new YesNoField(&resp3_enabled, true)},
+ {"hash-encoding-mode", false,
+ new EnumField<HashSubkeyEncodingMode>(&hash_encoding_mode,
hash_subkey_encoding_modes,
+ HashSubkeyEncodingMode::kLegacy)},
+ {"hash-length-mode", false,
+ new EnumField<HashLengthMode>(&hash_length_mode, hash_length_modes,
HashLengthMode::kAccurate)},
{"repl-namespace-enabled", false, new
YesNoField(&repl_namespace_enabled, false)},
{"proto-max-bulk-len", false,
new IntWithUnitField<uint64_t>(&proto_max_bulk_len, std::to_string(512
* MiB), 1 * MiB,
diff --git a/src/config/config.h b/src/config/config.h
index 159fb83b5..c17b331e7 100644
--- a/src/config/config.h
+++ b/src/config/config.h
@@ -180,6 +180,8 @@ struct Config {
int redis_databases = 0;
bool resp3_enabled = false;
int log_retention_days;
+ HashSubkeyEncodingMode hash_encoding_mode = HashSubkeyEncodingMode::kLegacy;
+ HashLengthMode hash_length_mode = HashLengthMode::kAccurate;
// load_tokens is used to buffer the tokens when loading,
// don't use it to authenticate or rewrite the configuration file.
diff --git a/src/search/indexer.cc b/src/search/indexer.cc
index 730a59089..07ece4990 100644
--- a/src/search/indexer.cc
+++ b/src/search/indexer.cc
@@ -43,7 +43,7 @@ StatusOr<FieldValueRetriever>
FieldValueRetriever::Create(IndexOnDataType type,
std::string ns_key = db.AppendNamespacePrefix(key);
HashMetadata metadata(false);
- auto s = db.GetMetadata(ctx, ns_key, &metadata);
+ auto s = db.getMetadata(ctx, ns_key, &metadata);
if (!s.ok()) return {s.IsNotFound() ? Status::NotFound : Status::NotOK,
s.ToString()};
return FieldValueRetriever(db, metadata, key);
} else if (type == IndexOnDataType::JSON) {
@@ -98,7 +98,7 @@ StatusOr<kqir::Value>
FieldValueRetriever::ParseFromJson(const jsoncons::json &v
}
}
-StatusOr<kqir::Value> FieldValueRetriever::ParseFromHash(const std::string
&value,
+StatusOr<kqir::Value> FieldValueRetriever::ParseFromHash(std::string_view
value,
const
redis::IndexFieldMetadata *type) {
if (auto numeric [[maybe_unused]] = dynamic_cast<const
redis::NumericFieldMetadata *>(type)) {
auto num = GET_OR_RET(ParseFloat(value));
@@ -137,7 +137,11 @@ StatusOr<kqir::Value>
FieldValueRetriever::Retrieve(engine::Context &ctx, std::s
if (s.IsNotFound()) return {Status::NotFound, s.ToString()};
if (!s.ok()) return {Status::NotOK, s.ToString()};
- return ParseFromHash(value, type);
+ Slice decoded_value(value);
+ s = metadata.DecodeSubkeyValue(&decoded_value);
+ if (!s.ok()) return {Status::NotOK, s.ToString()};
+
+ return ParseFromHash(decoded_value.ToStringView(), type);
} else if (std::holds_alternative<JsonData>(db)) {
auto &value = std::get<JsonData>(db);
diff --git a/src/search/indexer.h b/src/search/indexer.h
index b6d004310..18942029a 100644
--- a/src/search/indexer.h
+++ b/src/search/indexer.h
@@ -67,7 +67,7 @@ struct FieldValueRetriever {
StatusOr<kqir::Value> Retrieve(engine::Context &ctx, std::string_view field,
const redis::IndexFieldMetadata *type);
static StatusOr<kqir::Value> ParseFromJson(const jsoncons::json &value,
const redis::IndexFieldMetadata *type);
- static StatusOr<kqir::Value> ParseFromHash(const std::string &value, const
redis::IndexFieldMetadata *type);
+ static StatusOr<kqir::Value> ParseFromHash(std::string_view value, const
redis::IndexFieldMetadata *type);
};
struct IndexUpdater {
diff --git a/src/storage/redis_db.cc b/src/storage/redis_db.cc
index 99b3d96a3..1452d01ee 100644
--- a/src/storage/redis_db.cc
+++ b/src/storage/redis_db.cc
@@ -558,42 +558,7 @@ rocksdb::Status Database::KeyExist(engine::Context &ctx,
const std::string &key)
rocksdb::Status SubKeyScanner::Scan(engine::Context &ctx, RedisType type,
const Slice &user_key,
const std::string &cursor, uint64_t limit,
const std::string &subkey_prefix,
std::vector<std::string> *keys,
std::vector<std::string> *values) {
- uint64_t cnt = 0;
- std::string ns_key = AppendNamespacePrefix(user_key);
- Metadata metadata(type, false);
- rocksdb::Status s = GetMetadata(ctx, {type}, ns_key, &metadata);
- if (!s.ok()) return s;
-
- auto iter = util::UniqueIterator(ctx, ctx.DefaultScanOptions());
- std::string match_prefix_key =
- InternalKey(ns_key, subkey_prefix, metadata.version,
storage_->IsSlotIdEncoded()).Encode();
-
- std::string start_key;
- if (!cursor.empty()) {
- start_key = InternalKey(ns_key, cursor, metadata.version,
storage_->IsSlotIdEncoded()).Encode();
- } else {
- start_key = match_prefix_key;
- }
- for (iter->Seek(start_key); iter->Valid(); iter->Next()) {
- if (!cursor.empty() && iter->key() == start_key) {
- // if cursor is not empty, then we need to skip start_key
- // because we already return that key in the last scan
- continue;
- }
- if (!iter->key().starts_with(match_prefix_key)) {
- break;
- }
- InternalKey ikey(iter->key(), storage_->IsSlotIdEncoded());
- keys->emplace_back(ikey.GetSubKey().ToString());
- if (values != nullptr) {
- values->emplace_back(iter->value().ToString());
- }
- cnt++;
- if (limit > 0 && cnt >= limit) {
- break;
- }
- }
- return iter->status();
+ return scanSubkeys<Metadata>(ctx, type, user_key, cursor, limit,
subkey_prefix, keys, values);
}
RedisType WriteBatchLogData::GetRedisType() const { return type_; }
diff --git a/src/storage/redis_db.h b/src/storage/redis_db.h
index 506dc250b..8986790a2 100644
--- a/src/storage/redis_db.h
+++ b/src/storage/redis_db.h
@@ -22,11 +22,13 @@
#include <optional>
#include <string>
+#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
#include "cluster/cluster_defs.h"
+#include "common/db_util.h"
#include "redis_metadata.h"
#include "storage.h"
@@ -182,6 +184,68 @@ class SubKeyScanner : public redis::Database {
rocksdb::Status Scan(engine::Context &ctx, RedisType type, const Slice
&user_key, const std::string &cursor,
uint64_t limit, const std::string &subkey_prefix,
std::vector<std::string> *keys,
std::vector<std::string> *values = nullptr);
+
+ protected:
+ struct RawSubkeyValueAdapter {
+ template <typename MetadataT>
+ rocksdb::Status operator()(const MetadataT &, Slice *) const {
+ return rocksdb::Status::OK();
+ }
+ };
+
+ template <typename MetadataT>
+ static MetadataT createScanMetadata(RedisType type) {
+ if constexpr (std::is_same_v<MetadataT, Metadata>) {
+ return Metadata(type, false);
+ } else {
+ return MetadataT(false);
+ }
+ }
+
+ template <typename MetadataT, typename ValueAdapter = RawSubkeyValueAdapter>
+ rocksdb::Status scanSubkeys(engine::Context &ctx, RedisType type, const
Slice &user_key, const std::string &cursor,
+ uint64_t limit, const std::string
&subkey_prefix, std::vector<std::string> *keys,
+ std::vector<std::string> *values = nullptr,
ValueAdapter value_adapter = {}) {
+ uint64_t cnt = 0;
+ std::string ns_key = AppendNamespacePrefix(user_key);
+ auto metadata = createScanMetadata<MetadataT>(type);
+ rocksdb::Status s = GetMetadata(ctx, {type}, ns_key, &metadata);
+ if (!s.ok()) return s;
+
+ auto iter = util::UniqueIterator(ctx, ctx.DefaultScanOptions());
+ std::string match_prefix_key =
+ InternalKey(ns_key, subkey_prefix, metadata.version,
storage_->IsSlotIdEncoded()).Encode();
+
+ std::string start_key;
+ if (!cursor.empty()) {
+ start_key = InternalKey(ns_key, cursor, metadata.version,
storage_->IsSlotIdEncoded()).Encode();
+ } else {
+ start_key = match_prefix_key;
+ }
+ for (iter->Seek(start_key); iter->Valid(); iter->Next()) {
+ if (!cursor.empty() && iter->key() == start_key) {
+ // if cursor is not empty, then we need to skip start_key
+ // because we already return that key in the last scan
+ continue;
+ }
+ if (!iter->key().starts_with(match_prefix_key)) {
+ break;
+ }
+ InternalKey ikey(iter->key(), storage_->IsSlotIdEncoded());
+ keys->emplace_back(ikey.GetSubKey().ToString());
+ if (values != nullptr) {
+ Slice value(iter->value());
+ s = value_adapter(metadata, &value);
+ if (!s.ok()) return s;
+ values->emplace_back(value.data(), value.size());
+ }
+ cnt++;
+ if (limit > 0 && cnt >= limit) {
+ break;
+ }
+ }
+ return iter->status();
+ }
};
class WriteBatchLogData {
diff --git a/src/storage/redis_metadata.cc b/src/storage/redis_metadata.cc
index 692f8804d..6945a0a6d 100644
--- a/src/storage/redis_metadata.cc
+++ b/src/storage/redis_metadata.cc
@@ -38,6 +38,7 @@ const int VersionCounterBits = 11;
static std::atomic<uint64_t> version_counter_ = 0;
constexpr const char *kErrMetadataTooShort = "metadata is too short";
+constexpr const char *kErrHashSubkeyValueTooShort = "hash subkey value is too
short";
InternalKey::InternalKey(Slice input, bool slot_id_encoded) :
slot_id_encoded_(slot_id_encoded) {
uint32_t key_size = 0;
@@ -339,6 +340,80 @@ bool Metadata::IsEmptyableType() const {
bool Metadata::Expired() const { return ExpireAt(util::GetTimeStampMS()); }
+void HashMetadata::Encode(std::string *dst) const {
+ Metadata::Encode(dst);
+ if (IsLegacySubkeyEncoding()) {
+ return;
+ }
+
+ PutFixed8(dst, static_cast<uint8_t>(mode));
+ PutFixed64(dst, expsz);
+ PutFixed64(dst, lower);
+ PutFixed64(dst, upper);
+}
+
+rocksdb::Status HashMetadata::Decode(Slice *input) {
+ if (auto s = Metadata::Decode(input); !s.ok()) {
+ return s;
+ }
+
+ if (input->empty()) {
+ mode = HashSubkeyEncodingMode::kLegacy;
+ expsz = 0;
+ lower = 0;
+ upper = 0;
+ return rocksdb::Status::OK();
+ }
+
+ if (input->size() < 1 + 8 + 8 + 8) {
+ return rocksdb::Status::InvalidArgument(kErrMetadataTooShort);
+ }
+
+ uint8_t encoded_mode = 0;
+ GetFixed8(input, &encoded_mode);
+ if (encoded_mode >
static_cast<uint8_t>(HashSubkeyEncodingMode::kFieldExpiration)) {
+ return rocksdb::Status::InvalidArgument("invalid hash subkey encoding
mode");
+ }
+
+ mode = static_cast<HashSubkeyEncodingMode>(encoded_mode);
+ GetFixed64(input, &expsz);
+ GetFixed64(input, &lower);
+ GetFixed64(input, &upper);
+ return rocksdb::Status::OK();
+}
+
+std::string HashMetadata::EncodeSubkeyValue(Slice value, uint64_t expire)
const {
+ if (IsLegacySubkeyEncoding()) {
+ return value.ToString();
+ }
+
+ std::string encoded;
+ encoded.reserve(kFieldExpirationPrefixSize + value.size());
+ PutFixed64(&encoded, expire);
+ encoded.append(value.data(), value.size());
+ return encoded;
+}
+
+rocksdb::Status HashMetadata::DecodeSubkeyValue(Slice *value, uint64_t
*expire) const {
+ if (IsLegacySubkeyEncoding()) {
+ if (expire != nullptr) {
+ *expire = 0;
+ }
+ return rocksdb::Status::OK();
+ }
+
+ if (value->size() < kFieldExpirationPrefixSize) {
+ return rocksdb::Status::InvalidArgument(kErrHashSubkeyValueTooShort);
+ }
+
+ uint64_t encoded_expire = 0;
+ GetFixed64(value, &encoded_expire);
+ if (expire != nullptr) {
+ *expire = encoded_expire;
+ }
+ return rocksdb::Status::OK();
+}
+
ListMetadata::ListMetadata(bool generate_version)
: Metadata(kRedisList, generate_version), head(UINT64_MAX / 2), tail(head)
{}
diff --git a/src/storage/redis_metadata.h b/src/storage/redis_metadata.h
index fd80e5a5b..2e79cf232 100644
--- a/src/storage/redis_metadata.h
+++ b/src/storage/redis_metadata.h
@@ -103,6 +103,16 @@ enum RedisCommand {
constexpr const char *kErrMsgWrongType = "WRONGTYPE Operation against a key
holding the wrong kind of value";
constexpr const char *kErrMsgKeyExpired = "the key was expired";
+enum class HashSubkeyEncodingMode : uint8_t {
+ kLegacy = 0,
+ kFieldExpiration = 1,
+};
+
+enum class HashLengthMode : uint8_t {
+ kAccurate = 0,
+ kApproximate = 1,
+};
+
using rocksdb::Slice;
struct KeyNumStats {
@@ -211,7 +221,25 @@ class Metadata {
class HashMetadata : public Metadata {
public:
- explicit HashMetadata(bool generate_version = true) : Metadata(kRedisHash,
generate_version) {}
+ static constexpr size_t kFieldExpirationPrefixSize = sizeof(uint64_t);
+
+ HashSubkeyEncodingMode mode = HashSubkeyEncodingMode::kLegacy;
+ uint64_t expsz = 0;
+ uint64_t lower = 0;
+ uint64_t upper = 0;
+
+ explicit HashMetadata(bool generate_version = true, HashSubkeyEncodingMode
mode = HashSubkeyEncodingMode::kLegacy)
+ : Metadata(kRedisHash, generate_version), mode(mode) {}
+
+ bool IsLegacySubkeyEncoding() const { return mode ==
HashSubkeyEncodingMode::kLegacy; }
+ bool IsFieldExpirationEncoding() const { return mode ==
HashSubkeyEncodingMode::kFieldExpiration; }
+
+ [[nodiscard]] std::string EncodeSubkeyValue(Slice value, uint64_t expire =
0) const;
+ [[nodiscard]] rocksdb::Status DecodeSubkeyValue(Slice *value, uint64_t
*expire = nullptr) const;
+
+ void Encode(std::string *dst) const override;
+ using Metadata::Decode;
+ rocksdb::Status Decode(Slice *input) override;
};
class SetMetadata : public Metadata {
diff --git a/src/types/redis_hash.cc b/src/types/redis_hash.cc
index 26cdc43ab..be481110b 100644
--- a/src/types/redis_hash.cc
+++ b/src/types/redis_hash.cc
@@ -35,16 +35,28 @@
namespace redis {
-rocksdb::Status Hash::GetMetadata(engine::Context &ctx, const Slice &ns_key,
HashMetadata *metadata) {
+HashMetadata Hash::createMetadataForWrite(bool generate_version) const {
+ return HashMetadata(generate_version,
storage_->GetConfig()->hash_encoding_mode);
+}
+
+rocksdb::Status Hash::getMetadata(engine::Context &ctx, const Slice &ns_key,
HashMetadata *metadata) {
return Database::GetMetadata(ctx, {kRedisHash}, ns_key, metadata);
}
+rocksdb::Status Hash::getRawValue(engine::Context &ctx, const std::string
&sub_key, std::string *value) {
+ return storage_->Get(ctx, ctx.GetReadOptions(), sub_key, value);
+}
+
+rocksdb::Status Hash::decodeValue(const HashMetadata &metadata, Slice *value,
uint64_t *expire) {
+ return metadata.DecodeSubkeyValue(value, expire);
+}
+
rocksdb::Status Hash::Size(engine::Context &ctx, const Slice &user_key,
uint64_t *size) {
*size = 0;
std::string ns_key = AppendNamespacePrefix(user_key);
HashMetadata metadata(false);
- rocksdb::Status s = GetMetadata(ctx, ns_key, &metadata);
+ rocksdb::Status s = getMetadata(ctx, ns_key, &metadata);
if (!s.ok()) return s;
*size = metadata.size;
return rocksdb::Status::OK();
@@ -53,11 +65,17 @@ rocksdb::Status Hash::Size(engine::Context &ctx, const
Slice &user_key, uint64_t
rocksdb::Status Hash::Get(engine::Context &ctx, const Slice &user_key, const
Slice &field, std::string *value) {
std::string ns_key = AppendNamespacePrefix(user_key);
HashMetadata metadata(false);
- rocksdb::Status s = GetMetadata(ctx, ns_key, &metadata);
+ rocksdb::Status s = getMetadata(ctx, ns_key, &metadata);
if (!s.ok()) return s;
- rocksdb::ReadOptions read_options;
std::string sub_key = InternalKey(ns_key, field, metadata.version,
storage_->IsSlotIdEncoded()).Encode();
- return storage_->Get(ctx, ctx.GetReadOptions(), sub_key, value);
+ std::string raw_value;
+ s = getRawValue(ctx, sub_key, &raw_value);
+ if (!s.ok()) return s;
+ Slice payload(raw_value);
+ s = decodeValue(metadata, &payload);
+ if (!s.ok()) return s;
+ value->assign(payload.data(), payload.size());
+ return rocksdb::Status::OK();
}
rocksdb::Status Hash::IncrBy(engine::Context &ctx, const Slice &user_key,
const Slice &field, int64_t increment,
@@ -67,17 +85,21 @@ rocksdb::Status Hash::IncrBy(engine::Context &ctx, const
Slice &user_key, const
std::string ns_key = AppendNamespacePrefix(user_key);
- HashMetadata metadata;
- rocksdb::Status s = GetMetadata(ctx, ns_key, &metadata);
+ HashMetadata metadata = createMetadataForWrite();
+ rocksdb::Status s = getMetadata(ctx, ns_key, &metadata);
if (!s.ok() && !s.IsNotFound()) return s;
std::string sub_key = InternalKey(ns_key, field, metadata.version,
storage_->IsSlotIdEncoded()).Encode();
if (s.ok()) {
- std::string value_bytes;
- s = storage_->Get(ctx, ctx.GetReadOptions(), sub_key, &value_bytes);
+ std::string raw_value;
+ Slice value_bytes;
+ s = getRawValue(ctx, sub_key, &raw_value);
if (!s.ok() && !s.IsNotFound()) return s;
if (s.ok()) {
- auto parse_result = ParseInt<int64_t>(value_bytes, 10);
+ value_bytes = Slice(raw_value);
+ s = decodeValue(metadata, &value_bytes);
+ if (!s.ok()) return s;
+ auto parse_result = ParseInt<int64_t>(value_bytes.ToStringView(), 10);
if (!parse_result) {
return rocksdb::Status::InvalidArgument(parse_result.Msg());
}
@@ -98,7 +120,8 @@ rocksdb::Status Hash::IncrBy(engine::Context &ctx, const
Slice &user_key, const
WriteBatchLogData log_data(kRedisHash);
s = batch->PutLogData(log_data.Encode());
if (!s.ok()) return s;
- s = batch->Put(sub_key, std::to_string(*new_value));
+ std::string encoded_value =
metadata.EncodeSubkeyValue(std::to_string(*new_value));
+ s = batch->Put(sub_key, encoded_value);
if (!s.ok()) return s;
if (!exists) {
metadata.size += 1;
@@ -117,17 +140,21 @@ rocksdb::Status Hash::IncrByFloat(engine::Context &ctx,
const Slice &user_key, c
std::string ns_key = AppendNamespacePrefix(user_key);
- HashMetadata metadata;
- rocksdb::Status s = GetMetadata(ctx, ns_key, &metadata);
+ HashMetadata metadata = createMetadataForWrite();
+ rocksdb::Status s = getMetadata(ctx, ns_key, &metadata);
if (!s.ok() && !s.IsNotFound()) return s;
std::string sub_key = InternalKey(ns_key, field, metadata.version,
storage_->IsSlotIdEncoded()).Encode();
if (s.ok()) {
- std::string value_bytes;
- s = storage_->Get(ctx, ctx.GetReadOptions(), sub_key, &value_bytes);
+ std::string raw_value;
+ Slice value_bytes;
+ s = getRawValue(ctx, sub_key, &raw_value);
if (!s.ok() && !s.IsNotFound()) return s;
if (s.ok()) {
- auto value_stat = ParseFloat(value_bytes);
+ value_bytes = Slice(raw_value);
+ s = decodeValue(metadata, &value_bytes);
+ if (!s.ok()) return s;
+ auto value_stat = ParseFloat(value_bytes.ToStringView());
if (!value_stat || isspace(value_bytes[0])) {
return rocksdb::Status::InvalidArgument("value is not a number");
}
@@ -145,7 +172,8 @@ rocksdb::Status Hash::IncrByFloat(engine::Context &ctx,
const Slice &user_key, c
WriteBatchLogData log_data(kRedisHash);
s = batch->PutLogData(log_data.Encode());
if (!s.ok()) return s;
- s = batch->Put(sub_key, util::Float2String(*new_value));
+ std::string encoded_value =
metadata.EncodeSubkeyValue(util::Float2String(*new_value));
+ s = batch->Put(sub_key, encoded_value);
if (!s.ok()) return s;
if (!exists) {
metadata.size += 1;
@@ -164,7 +192,7 @@ rocksdb::Status Hash::MGet(engine::Context &ctx, const
Slice &user_key, const st
std::string ns_key = AppendNamespacePrefix(user_key);
HashMetadata metadata(false);
- rocksdb::Status s = GetMetadata(ctx, ns_key, &metadata);
+ rocksdb::Status s = getMetadata(ctx, ns_key, &metadata);
if (!s.ok()) {
return s;
}
@@ -189,7 +217,14 @@ rocksdb::Status Hash::MGet(engine::Context &ctx, const
Slice &user_key, const st
values_vector.data(), statuses_vector.data());
for (size_t i = 0; i < keys.size(); i++) {
if (!statuses_vector[i].ok() && !statuses_vector[i].IsNotFound()) return
statuses_vector[i];
- values->emplace_back(values_vector[i].ToString());
+ if (statuses_vector[i].ok()) {
+ Slice value(values_vector[i]);
+ s = decodeValue(metadata, &value);
+ if (!s.ok()) return s;
+ values->emplace_back(value.data(), value.size());
+ } else {
+ values->emplace_back("");
+ }
statuses->emplace_back(statuses_vector[i]);
}
return rocksdb::Status::OK();
@@ -211,7 +246,7 @@ rocksdb::Status Hash::Delete(engine::Context &ctx, const
Slice &user_key, const
auto s = batch->PutLogData(log_data.Encode());
if (!s.ok()) return s;
- s = GetMetadata(ctx, ns_key, &metadata);
+ s = getMetadata(ctx, ns_key, &metadata);
if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
std::string value;
@@ -244,8 +279,8 @@ rocksdb::Status Hash::MSet(engine::Context &ctx, const
Slice &user_key, const st
*added_cnt = 0;
std::string ns_key = AppendNamespacePrefix(user_key);
- HashMetadata metadata;
- rocksdb::Status s = GetMetadata(ctx, ns_key, &metadata);
+ HashMetadata metadata = createMetadataForWrite();
+ rocksdb::Status s = getMetadata(ctx, ns_key, &metadata);
if (!s.ok() && !s.IsNotFound()) return s;
bool ttl_updated = false;
if (expire > 0 && metadata.expire != expire) {
@@ -292,7 +327,13 @@ rocksdb::Status Hash::MSet(engine::Context &ctx, const
Slice &user_key, const st
return field_status;
}
if (field_status.ok()) {
- if (nx || values_vector[field_index] == values[field_index]) {
+ if (nx) {
+ continue;
+ }
+ Slice existing_value(values_vector[field_index]);
+ s = decodeValue(metadata, &existing_value);
+ if (!s.ok()) return s;
+ if (existing_value.ToStringView() == values[field_index]) {
continue;
}
exists = true;
@@ -303,7 +344,8 @@ rocksdb::Status Hash::MSet(engine::Context &ctx, const
Slice &user_key, const st
added++;
}
- s = batch->Put(field_key, values[field_index]);
+ std::string encoded_value =
metadata.EncodeSubkeyValue(values[field_index]);
+ s = batch->Put(field_key, encoded_value);
if (!s.ok()) return s;
}
@@ -327,7 +369,7 @@ rocksdb::Status Hash::RangeByLex(engine::Context &ctx,
const Slice &user_key, co
}
std::string ns_key = AppendNamespacePrefix(user_key);
HashMetadata metadata(false);
- rocksdb::Status s = GetMetadata(ctx, ns_key, &metadata);
+ rocksdb::Status s = getMetadata(ctx, ns_key, &metadata);
if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
std::string start_member = spec.reversed ? spec.max : spec.min;
@@ -370,7 +412,10 @@ rocksdb::Status Hash::RangeByLex(engine::Context &ctx,
const Slice &user_key, co
}
if (spec.offset >= 0 && pos++ < spec.offset) continue;
- field_values->emplace_back(ikey.GetSubKey().ToString(),
iter->value().ToString());
+ Slice value(iter->value());
+ s = decodeValue(metadata, &value);
+ if (!s.ok()) return s;
+ field_values->emplace_back(ikey.GetSubKey().ToString(),
std::string(value.data(), value.size()));
if (spec.count > 0 && field_values->size() >=
static_cast<unsigned>(spec.count)) break;
}
return rocksdb::Status::OK();
@@ -382,7 +427,7 @@ rocksdb::Status Hash::GetAll(engine::Context &ctx, const
Slice &user_key, std::v
std::string ns_key = AppendNamespacePrefix(user_key);
HashMetadata metadata(false);
- rocksdb::Status s = GetMetadata(ctx, ns_key, &metadata);
+ rocksdb::Status s = getMetadata(ctx, ns_key, &metadata);
if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
std::string prefix_key = InternalKey(ns_key, "", metadata.version,
storage_->IsSlotIdEncoded()).Encode();
@@ -399,10 +444,16 @@ rocksdb::Status Hash::GetAll(engine::Context &ctx, const
Slice &user_key, std::v
InternalKey ikey(iter->key(), storage_->IsSlotIdEncoded());
field_values->emplace_back(ikey.GetSubKey().ToString(), "");
} else if (type == HashFetchType::kOnlyValue) {
- field_values->emplace_back("", iter->value().ToString());
+ Slice value(iter->value());
+ s = decodeValue(metadata, &value);
+ if (!s.ok()) return s;
+ field_values->emplace_back("", std::string(value.data(), value.size()));
} else {
InternalKey ikey(iter->key(), storage_->IsSlotIdEncoded());
- field_values->emplace_back(ikey.GetSubKey().ToString(),
iter->value().ToString());
+ Slice value(iter->value());
+ s = decodeValue(metadata, &value);
+ if (!s.ok()) return s;
+ field_values->emplace_back(ikey.GetSubKey().ToString(),
std::string(value.data(), value.size()));
}
}
return rocksdb::Status::OK();
@@ -411,7 +462,9 @@ rocksdb::Status Hash::GetAll(engine::Context &ctx, const
Slice &user_key, std::v
rocksdb::Status Hash::Scan(engine::Context &ctx, const Slice &user_key, const
std::string &cursor, uint64_t limit,
const std::string &field_prefix,
std::vector<std::string> *fields,
std::vector<std::string> *values) {
- return SubKeyScanner::Scan(ctx, kRedisHash, user_key, cursor, limit,
field_prefix, fields, values);
+ return scanSubkeys<HashMetadata>(
+ ctx, kRedisHash, user_key, cursor, limit, field_prefix, fields, values,
+ [](const HashMetadata &metadata, Slice *value) { return
metadata.DecodeSubkeyValue(value); });
}
rocksdb::Status Hash::RandField(engine::Context &ctx, const Slice &user_key,
int64_t command_count,
@@ -421,7 +474,7 @@ rocksdb::Status Hash::RandField(engine::Context &ctx, const
Slice &user_key, int
std::string ns_key = AppendNamespacePrefix(user_key);
HashMetadata metadata(/*generate_version=*/false);
- rocksdb::Status s = GetMetadata(ctx, ns_key, &metadata);
+ rocksdb::Status s = getMetadata(ctx, ns_key, &metadata);
if (!s.ok()) return s;
std::vector<FieldValue> samples;
diff --git a/src/types/redis_hash.h b/src/types/redis_hash.h
index 94f722c4f..400c2401b 100644
--- a/src/types/redis_hash.h
+++ b/src/types/redis_hash.h
@@ -70,7 +70,10 @@ class Hash : public SubKeyScanner {
std::vector<FieldValue> *field_values,
HashFetchType type = HashFetchType::kOnlyKey);
private:
- rocksdb::Status GetMetadata(engine::Context &ctx, const Slice &ns_key,
HashMetadata *metadata);
+ [[nodiscard]] HashMetadata createMetadataForWrite(bool generate_version =
true) const;
+ rocksdb::Status getMetadata(engine::Context &ctx, const Slice &ns_key,
HashMetadata *metadata);
+ rocksdb::Status getRawValue(engine::Context &ctx, const std::string
&sub_key, std::string *value);
+ static rocksdb::Status decodeValue(const HashMetadata &metadata, Slice
*value, uint64_t *expire = nullptr);
friend struct FieldValueRetriever;
};
diff --git a/tests/cppunit/config_test.cc b/tests/cppunit/config_test.cc
index 3c78a48f0..aff85b86d 100644
--- a/tests/cppunit/config_test.cc
+++ b/tests/cppunit/config_test.cc
@@ -61,6 +61,8 @@ TEST(Config, GetAndSet) {
{"profiling-sample-record-threshold-ms", "50"},
{"profiling-sample-commands", "get,set"},
{"backup-dir", "test_dir/backup"},
+ {"hash-encoding-mode", "field-expiration"},
+ {"hash-length-mode", "approximate"},
{"rocksdb.compression", "no"},
{"rocksdb.max_open_files", "1234"},
diff --git a/tests/cppunit/metadata_test.cc b/tests/cppunit/metadata_test.cc
index ffbaa88ba..dfd8a43bb 100644
--- a/tests/cppunit/metadata_test.cc
+++ b/tests/cppunit/metadata_test.cc
@@ -66,6 +66,63 @@ TEST(Metadata, EncodeAndDecode) {
ASSERT_EQ(list_md, list_md1);
}
+TEST(HashMetadata, DecodeLegacyMetadataWithoutExtensions) {
+ Metadata legacy_md(kRedisHash, false);
+ legacy_md.expire = 123000;
+ legacy_md.version = 99;
+ legacy_md.size = 7;
+
+ std::string encoded_bytes;
+ legacy_md.Encode(&encoded_bytes);
+
+ HashMetadata decoded(false, HashSubkeyEncodingMode::kFieldExpiration);
+ ASSERT_TRUE(decoded.Decode(encoded_bytes).ok());
+ EXPECT_EQ(decoded.expire, legacy_md.expire);
+ EXPECT_EQ(decoded.version, legacy_md.version);
+ EXPECT_EQ(decoded.size, legacy_md.size);
+ EXPECT_EQ(decoded.mode, HashSubkeyEncodingMode::kLegacy);
+ EXPECT_EQ(decoded.expsz, 0);
+ EXPECT_EQ(decoded.lower, 0);
+ EXPECT_EQ(decoded.upper, 0);
+}
+
+TEST(HashMetadata, EncodeAndDecodeWithExtensions) {
+ HashMetadata metadata(false, HashSubkeyEncodingMode::kFieldExpiration);
+ metadata.expire = 123000;
+ metadata.version = 9;
+ metadata.size = 11;
+ metadata.expsz = 3;
+ metadata.lower = 1000;
+ metadata.upper = 2000;
+
+ std::string encoded_bytes;
+ metadata.Encode(&encoded_bytes);
+
+ HashMetadata decoded(false);
+ ASSERT_TRUE(decoded.Decode(encoded_bytes).ok());
+ EXPECT_EQ(decoded.expire, metadata.expire);
+ EXPECT_EQ(decoded.version, metadata.version);
+ EXPECT_EQ(decoded.size, metadata.size);
+ EXPECT_EQ(decoded.mode, HashSubkeyEncodingMode::kFieldExpiration);
+ EXPECT_EQ(decoded.expsz, metadata.expsz);
+ EXPECT_EQ(decoded.lower, metadata.lower);
+ EXPECT_EQ(decoded.upper, metadata.upper);
+}
+
+TEST(HashMetadata, EncodeAndDecodeSubkeyValueWithFieldExpirationMode) {
+ HashMetadata metadata(false, HashSubkeyEncodingMode::kFieldExpiration);
+ constexpr uint64_t expire = 123456789;
+
+ std::string encoded = metadata.EncodeSubkeyValue("value", expire);
+ EXPECT_EQ(encoded.size(), HashMetadata::kFieldExpirationPrefixSize +
std::string("value").size());
+
+ Slice decoded_value(encoded);
+ uint64_t decoded_expire = 0;
+ ASSERT_TRUE(metadata.DecodeSubkeyValue(&decoded_value,
&decoded_expire).ok());
+ EXPECT_EQ(decoded_value.ToStringView(), "value");
+ EXPECT_EQ(decoded_expire, expire);
+}
+
class RedisTypeTest : public TestBase {
public:
RedisTypeTest() {
diff --git a/tests/cppunit/types/hash_test.cc b/tests/cppunit/types/hash_test.cc
index f4cca4c34..e939e5ad5 100644
--- a/tests/cppunit/types/hash_test.cc
+++ b/tests/cppunit/types/hash_test.cc
@@ -21,7 +21,11 @@
#include <gtest/gtest.h>
#include <algorithm>
+#include <cassert>
#include <climits>
+#include <cstdint>
+#include <filesystem>
+#include <fstream>
#include <memory>
#include <random>
#include <string>
@@ -45,6 +49,64 @@ class RedisHashTest : public TestBase {
std::unique_ptr<redis::Hash> hash_;
};
+class RedisHashFieldExpirationEncodingTest : public ::testing::Test {
+ public:
+ RedisHashFieldExpirationEncodingTest(const
RedisHashFieldExpirationEncodingTest &) = delete;
+ RedisHashFieldExpirationEncodingTest &operator=(const
RedisHashFieldExpirationEncodingTest &) = delete;
+
+ protected:
+ RedisHashFieldExpirationEncodingTest() {
+ const char *path = "test_hash_field_expiration.conf";
+ unlink(path);
+ std::ofstream output_file(path, std::ios::out);
+ output_file << "hash-encoding-mode field-expiration\n";
+ output_file.close();
+
+ auto s = config_.Load(CLIOptions(path));
+ assert(s.IsOK());
+ config_.db_dir = "testdb_hash_field_expiration";
+ config_.rocks_db.compression = rocksdb::CompressionType::kNoCompression;
+ config_.rocks_db.write_buffer_size = 1;
+ config_.rocks_db.block_size = 100;
+
+ storage_ = std::make_unique<engine::Storage>(&config_);
+ s = storage_->Open();
+ assert(s.IsOK());
+
+ ctx_ = std::make_unique<engine::Context>(storage_.get());
+ hash_ = std::make_unique<redis::Hash>(storage_.get(), "hash_ns");
+ db_ = std::make_unique<redis::Database>(storage_.get(), "hash_ns");
+ }
+
+ ~RedisHashFieldExpirationEncodingTest() override {
+ ctx_.reset();
+ hash_.reset();
+ db_.reset();
+ storage_.reset();
+
+ std::error_code ec;
+ std::filesystem::remove_all(config_.db_dir, ec);
+ unlink("test_hash_field_expiration.conf");
+ }
+
+ std::string rawHashValue(const std::string &key, const std::string &field,
HashMetadata *metadata) {
+ std::string ns_key = db_->AppendNamespacePrefix(key);
+ auto s = db_->GetMetadata(*ctx_, {kRedisHash}, ns_key, metadata);
+ assert(s.ok());
+ std::string sub_key = InternalKey(ns_key, field, metadata->version,
storage_->IsSlotIdEncoded()).Encode();
+ std::string raw_value;
+ s = storage_->Get(*ctx_, ctx_->GetReadOptions(), sub_key, &raw_value);
+ assert(s.ok());
+ return raw_value;
+ }
+
+ Config config_;
+ std::unique_ptr<engine::Storage> storage_;
+ std::unique_ptr<engine::Context> ctx_;
+ std::unique_ptr<redis::Hash> hash_;
+ std::unique_ptr<redis::Database> db_;
+};
+
TEST_F(RedisHashTest, GetAndSet) {
uint64_t ret = 0;
for (size_t i = 0; i < fields_.size(); i++) {
@@ -176,6 +238,48 @@ TEST_F(RedisHashTest, HGetAll) {
s = hash_->Del(*ctx_, key_);
}
+TEST_F(RedisHashFieldExpirationEncodingTest,
StoreAndScanValuesWithModeOneEncoding) {
+ const Slice key = "mode-one-hash";
+ const Slice field = "field-1";
+ const Slice value = "value-1";
+
+ uint64_t added = 0;
+ auto s = hash_->Set(*ctx_, key, field, value, &added);
+ ASSERT_TRUE(s.ok());
+ ASSERT_EQ(added, 1);
+
+ HashMetadata metadata(false);
+ std::string raw_value = rawHashValue(key.ToString(), field.ToString(),
&metadata);
+ EXPECT_EQ(metadata.mode, HashSubkeyEncodingMode::kFieldExpiration);
+ EXPECT_EQ(metadata.expsz, 0);
+ EXPECT_EQ(raw_value.size(), HashMetadata::kFieldExpirationPrefixSize +
value.size());
+
+ Slice decoded_value(raw_value);
+ uint64_t field_expire = UINT64_MAX;
+ ASSERT_TRUE(metadata.DecodeSubkeyValue(&decoded_value, &field_expire).ok());
+ EXPECT_EQ(decoded_value.ToStringView(), value.ToStringView());
+ EXPECT_EQ(field_expire, 0);
+
+ std::string got;
+ s = hash_->Get(*ctx_, key, field, &got);
+ ASSERT_TRUE(s.ok());
+ EXPECT_EQ(got, value.ToString());
+
+ std::vector<std::string> fields;
+ std::vector<std::string> values;
+ s = hash_->Scan(*ctx_, key, "", 10, "", &fields, &values);
+ ASSERT_TRUE(s.ok());
+ ASSERT_EQ(fields, std::vector<std::string>({"field-1"}));
+ ASSERT_EQ(values, std::vector<std::string>({"value-1"}));
+
+ std::vector<FieldValue> field_values;
+ s = hash_->GetAll(*ctx_, key, &field_values);
+ ASSERT_TRUE(s.ok());
+ ASSERT_EQ(field_values.size(), 1);
+ EXPECT_EQ(field_values[0].field, "field-1");
+ EXPECT_EQ(field_values[0].value, "value-1");
+}
+
TEST_F(RedisHashTest, HIncr) {
int64_t value = 0;
Slice field("hash-incrby-invalid-field");
diff --git a/tests/gocase/unit/config/config_test.go
b/tests/gocase/unit/config/config_test.go
index b9e087e9b..d80218344 100644
--- a/tests/gocase/unit/config/config_test.go
+++ b/tests/gocase/unit/config/config_test.go
@@ -308,27 +308,91 @@ func TestGetConfigTxnContext(t *testing.T) {
func TestGenerateConfigsMatrix(t *testing.T) {
t.Parallel()
- configOptions := []util.ConfigOptions{
- {
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
- },
- {
- Name: "resp3-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
- },
- }
+ t.Run("Generate one by one matrix", func(t *testing.T) {
+ configOptions := []util.ConfigOptions{
+ {
+ Name: "resp3-enabled",
+ Options: []string{"yes"},
+ },
+ }
- configsMatrix, err := util.GenerateConfigsMatrix(configOptions)
+ configsMatrix, err := util.GenerateConfigsMatrix(configOptions)
- require.NoError(t, err)
- require.Equal(t, 4, len(configsMatrix))
- require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"txn-context-enabled": "yes", "resp3-enabled": "yes"})
- require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"txn-context-enabled": "yes", "resp3-enabled": "no"})
- require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"txn-context-enabled": "no", "resp3-enabled": "yes"})
- require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"txn-context-enabled": "no", "resp3-enabled": "no"})
+ require.NoError(t, err)
+ require.Equal(t, []util.KvrocksServerConfigs{
+ {"resp3-enabled": "yes"},
+ }, configsMatrix)
+ })
+
+ t.Run("Generate one by two matrix", func(t *testing.T) {
+ configOptions := []util.ConfigOptions{
+ {
+ Name: "txn-context-enabled",
+ Options: []string{"yes"},
+ },
+ {
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
+ },
+ }
+
+ configsMatrix, err := util.GenerateConfigsMatrix(configOptions)
+
+ require.NoError(t, err)
+ require.Equal(t, []util.KvrocksServerConfigs{
+ {"txn-context-enabled": "yes", "resp3-enabled": "yes"},
+ {"txn-context-enabled": "yes", "resp3-enabled": "no"},
+ }, configsMatrix)
+ })
+
+ t.Run("Generate two by two matrix", func(t *testing.T) {
+ configOptions := []util.ConfigOptions{
+ {
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
+ },
+ {
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
+ },
+ }
+
+ configsMatrix, err := util.GenerateConfigsMatrix(configOptions)
+
+ require.NoError(t, err)
+ require.Equal(t, 4, len(configsMatrix))
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"txn-context-enabled": "yes", "resp3-enabled": "yes"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"txn-context-enabled": "yes", "resp3-enabled": "no"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"txn-context-enabled": "no", "resp3-enabled": "yes"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"txn-context-enabled": "no", "resp3-enabled": "no"})
+ })
+
+ t.Run("Generate three by three matrix without reusing result maps",
func(t *testing.T) {
+ configOptions := []util.ConfigOptions{
+ {
+ Name: "a",
+ Options: []string{"1", "2", "3"},
+ },
+ {
+ Name: "b",
+ Options: []string{"x", "y", "z"},
+ },
+ }
+
+ configsMatrix, err := util.GenerateConfigsMatrix(configOptions)
+
+ require.NoError(t, err)
+ require.Len(t, configsMatrix, 9)
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"a": "1", "b": "x"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"a": "1", "b": "y"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"a": "1", "b": "z"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"a": "2", "b": "x"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"a": "2", "b": "y"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"a": "2", "b": "z"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"a": "3", "b": "x"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"a": "3", "b": "y"})
+ require.Contains(t, configsMatrix,
util.KvrocksServerConfigs{"a": "3", "b": "z"})
+ })
}
func TestGetConfigSkipBlockCacheDeallocationOnClose(t *testing.T) {
diff --git a/tests/gocase/unit/geo/geo_test.go
b/tests/gocase/unit/geo/geo_test.go
index d3d8f0b1b..7113336f8 100644
--- a/tests/gocase/unit/geo/geo_test.go
+++ b/tests/gocase/unit/geo/geo_test.go
@@ -100,14 +100,12 @@ var geoAddAndGeoRangeservers []*util.KvrocksServer
func TestGeo(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
{
- Name: "resp3-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/kmetadata/kmetadata_test.go
b/tests/gocase/unit/kmetadata/kmetadata_test.go
index 87e5bdfdf..7960c4b9b 100644
--- a/tests/gocase/unit/kmetadata/kmetadata_test.go
+++ b/tests/gocase/unit/kmetadata/kmetadata_test.go
@@ -90,9 +90,8 @@ func ExtractKMetadataResponse(result interface{})
(*kMetadataResponse, error) {
func TestKMetadata(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "resp3-enabled",
- Options: []string{"yes"},
- ConfigType: util.YesNo,
+ Name: "resp3-enabled",
+ Options: []string{"yes"},
},
}
configsMatrix, err := util.GenerateConfigsMatrix(configOptions)
diff --git a/tests/gocase/unit/pubsub/pubsub_test.go
b/tests/gocase/unit/pubsub/pubsub_test.go
index 6c398757e..e9bc34b95 100644
--- a/tests/gocase/unit/pubsub/pubsub_test.go
+++ b/tests/gocase/unit/pubsub/pubsub_test.go
@@ -39,14 +39,12 @@ func receiveType[T any](t *testing.T, pubsub *redis.PubSub,
typ T) T {
func TestPubSub(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
{
- Name: "resp3-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/scripting/function_test.go
b/tests/gocase/unit/scripting/function_test.go
index 5d307b488..c505a7521 100644
--- a/tests/gocase/unit/scripting/function_test.go
+++ b/tests/gocase/unit/scripting/function_test.go
@@ -100,14 +100,12 @@ func decodeListLibResult(t *testing.T, v interface{})
ListLibResult {
func TestFunctions(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
{
- Name: "resp3-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/type/bloom/bloom_test.go
b/tests/gocase/unit/type/bloom/bloom_test.go
index 13009afd4..d6ac6af08 100644
--- a/tests/gocase/unit/type/bloom/bloom_test.go
+++ b/tests/gocase/unit/type/bloom/bloom_test.go
@@ -32,9 +32,8 @@ import (
func TestBloomInfo(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/type/hash/hash_test.go
b/tests/gocase/unit/type/hash/hash_test.go
index 1c972fa86..d5f23a3b7 100644
--- a/tests/gocase/unit/type/hash/hash_test.go
+++ b/tests/gocase/unit/type/hash/hash_test.go
@@ -52,17 +52,35 @@ func getVals(hash map[string]string) []string {
return r
}
+func runWithHashConfigs(t *testing.T, configOptions []util.ConfigOptions,
+ fn func(t *testing.T, configs util.KvrocksServerConfigs)) {
+ t.Helper()
+
+ configOptions = append(configOptions, util.ConfigOptions{
+ Name: "hash-encoding-mode",
+ Options: []string{"legacy", "field-expiration"},
+ })
+ configsMatrix, err := util.GenerateConfigsMatrix(configOptions)
+ require.NoError(t, err)
+
+ for _, configs := range configsMatrix {
+ fn(t, configs)
+ }
+}
+
func TestHash(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
+ },
+ {
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
},
{
- Name: "resp3-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "hash-encoding-mode",
+ Options: []string{"legacy", "field-expiration"},
},
}
@@ -993,107 +1011,122 @@ var testHash = func(t *testing.T, configs
util.KvrocksServerConfigs) {
}
func TestHGetAllWithRESP3(t *testing.T) {
- srv := util.StartServer(t, map[string]string{
- "resp3-enabled": "yes",
- })
- defer srv.Close()
+ runWithHashConfigs(t, []util.ConfigOptions{
+ {
+ Name: "resp3-enabled",
+ Options: []string{"yes"},
+ },
+ }, func(t *testing.T, configs util.KvrocksServerConfigs) {
+ srv := util.StartServer(t, configs)
+ defer srv.Close()
- rdb := srv.NewClient()
- defer func() { require.NoError(t, rdb.Close()) }()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
- ctx := context.Background()
+ ctx := context.Background()
- testKey := "test-hash-1"
- require.NoError(t, rdb.Del(ctx, testKey).Err())
- require.NoError(t, rdb.HSet(ctx, testKey, "key1", "value1", "key2",
"value2", "key3", "value3").Err())
- result, err := rdb.HGetAll(ctx, testKey).Result()
- require.NoError(t, err)
- require.Len(t, result, 3)
- require.EqualValues(t, map[string]string{
- "key1": "value1",
- "key2": "value2",
- "key3": "value3",
- }, result)
+ testKey := "test-hash-1"
+ require.NoError(t, rdb.Del(ctx, testKey).Err())
+ require.NoError(t, rdb.HSet(ctx, testKey, "key1", "value1",
"key2", "value2", "key3", "value3").Err())
+ result, err := rdb.HGetAll(ctx, testKey).Result()
+ require.NoError(t, err)
+ require.Len(t, result, 3)
+ require.EqualValues(t, map[string]string{
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3",
+ }, result)
+ })
}
func TestHashWithAsyncIOEnabled(t *testing.T) {
- srv := util.StartServer(t, map[string]string{
- "rocksdb.read_options.async_io": "yes",
- })
- defer srv.Close()
+ runWithHashConfigs(t, []util.ConfigOptions{
+ {
+ Name: "rocksdb.read_options.async_io",
+ Options: []string{"yes"},
+ },
+ }, func(t *testing.T, configs util.KvrocksServerConfigs) {
+ srv := util.StartServer(t, configs)
+ defer srv.Close()
- rdb := srv.NewClient()
- defer func() { require.NoError(t, rdb.Close()) }()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
- ctx := context.Background()
+ ctx := context.Background()
- t.Run("Test bug with large value after compaction", func(t *testing.T) {
- testKey := "test-hash-1"
- require.NoError(t, rdb.Del(ctx, testKey).Err())
+ t.Run("Test bug with large value after compaction", func(t
*testing.T) {
+ testKey := "test-hash-1"
+ require.NoError(t, rdb.Del(ctx, testKey).Err())
- src := rand.NewSource(time.Now().UnixNano())
- dd := make([]byte, 5000)
- for i := 1; i <= 50; i++ {
- for j := range dd {
- dd[j] = byte(src.Int63())
+ src := rand.NewSource(time.Now().UnixNano())
+ dd := make([]byte, 5000)
+ for i := 1; i <= 50; i++ {
+ for j := range dd {
+ dd[j] = byte(src.Int63())
+ }
+ key := util.RandString(10, 20, util.Alpha)
+ require.NoError(t, rdb.HSet(ctx, testKey, key,
string(dd)).Err())
}
- key := util.RandString(10, 20, util.Alpha)
- require.NoError(t, rdb.HSet(ctx, testKey, key,
string(dd)).Err())
- }
- require.EqualValues(t, 50, rdb.HLen(ctx, testKey).Val())
- require.Len(t, rdb.HGetAll(ctx, testKey).Val(), 50)
- require.Len(t, rdb.HKeys(ctx, testKey).Val(), 50)
- require.Len(t, rdb.HVals(ctx, testKey).Val(), 50)
+ require.EqualValues(t, 50, rdb.HLen(ctx, testKey).Val())
+ require.Len(t, rdb.HGetAll(ctx, testKey).Val(), 50)
+ require.Len(t, rdb.HKeys(ctx, testKey).Val(), 50)
+ require.Len(t, rdb.HVals(ctx, testKey).Val(), 50)
- require.NoError(t, rdb.Do(ctx, "COMPACT").Err())
+ require.NoError(t, rdb.Do(ctx, "COMPACT").Err())
- time.Sleep(5 * time.Second)
+ time.Sleep(5 * time.Second)
- require.EqualValues(t, 50, rdb.HLen(ctx, testKey).Val())
- require.Len(t, rdb.HGetAll(ctx, testKey).Val(), 50)
- require.Len(t, rdb.HKeys(ctx, testKey).Val(), 50)
- require.Len(t, rdb.HVals(ctx, testKey).Val(), 50)
+ require.EqualValues(t, 50, rdb.HLen(ctx, testKey).Val())
+ require.Len(t, rdb.HGetAll(ctx, testKey).Val(), 50)
+ require.Len(t, rdb.HKeys(ctx, testKey).Val(), 50)
+ require.Len(t, rdb.HVals(ctx, testKey).Val(), 50)
+ })
})
}
func TestHashWithAsyncIODisabled(t *testing.T) {
- srv := util.StartServer(t, map[string]string{
- "rocksdb.read_options.async_io": "no",
- })
- defer srv.Close()
+ runWithHashConfigs(t, []util.ConfigOptions{
+ {
+ Name: "rocksdb.read_options.async_io",
+ Options: []string{"no"},
+ },
+ }, func(t *testing.T, configs util.KvrocksServerConfigs) {
+ srv := util.StartServer(t, configs)
+ defer srv.Close()
- rdb := srv.NewClient()
- defer func() { require.NoError(t, rdb.Close()) }()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
- ctx := context.Background()
+ ctx := context.Background()
- t.Run("Test bug with large value after compaction", func(t *testing.T) {
- testKey := "test-hash-1"
- require.NoError(t, rdb.Del(ctx, testKey).Err())
+ t.Run("Test bug with large value after compaction", func(t
*testing.T) {
+ testKey := "test-hash-1"
+ require.NoError(t, rdb.Del(ctx, testKey).Err())
- src := rand.NewSource(time.Now().UnixNano())
- dd := make([]byte, 5000)
- for i := 1; i <= 50; i++ {
- for j := range dd {
- dd[j] = byte(src.Int63())
+ src := rand.NewSource(time.Now().UnixNano())
+ dd := make([]byte, 5000)
+ for i := 1; i <= 50; i++ {
+ for j := range dd {
+ dd[j] = byte(src.Int63())
+ }
+ key := util.RandString(10, 20, util.Alpha)
+ require.NoError(t, rdb.HSet(ctx, testKey, key,
string(dd)).Err())
}
- key := util.RandString(10, 20, util.Alpha)
- require.NoError(t, rdb.HSet(ctx, testKey, key,
string(dd)).Err())
- }
- require.EqualValues(t, 50, rdb.HLen(ctx, testKey).Val())
- require.Len(t, rdb.HGetAll(ctx, testKey).Val(), 50)
- require.Len(t, rdb.HKeys(ctx, testKey).Val(), 50)
- require.Len(t, rdb.HVals(ctx, testKey).Val(), 50)
+ require.EqualValues(t, 50, rdb.HLen(ctx, testKey).Val())
+ require.Len(t, rdb.HGetAll(ctx, testKey).Val(), 50)
+ require.Len(t, rdb.HKeys(ctx, testKey).Val(), 50)
+ require.Len(t, rdb.HVals(ctx, testKey).Val(), 50)
- require.NoError(t, rdb.Do(ctx, "COMPACT").Err())
+ require.NoError(t, rdb.Do(ctx, "COMPACT").Err())
- time.Sleep(5 * time.Second)
+ time.Sleep(5 * time.Second)
- require.EqualValues(t, 50, rdb.HLen(ctx, testKey).Val())
- require.Len(t, rdb.HGetAll(ctx, testKey).Val(), 50)
- require.Len(t, rdb.HKeys(ctx, testKey).Val(), 50)
- require.Len(t, rdb.HVals(ctx, testKey).Val(), 50)
+ require.EqualValues(t, 50, rdb.HLen(ctx, testKey).Val())
+ require.Len(t, rdb.HGetAll(ctx, testKey).Val(), 50)
+ require.Len(t, rdb.HKeys(ctx, testKey).Val(), 50)
+ require.Len(t, rdb.HVals(ctx, testKey).Val(), 50)
+ })
})
}
diff --git a/tests/gocase/unit/type/incr/incr_test.go
b/tests/gocase/unit/type/incr/incr_test.go
index 99a0c6b6b..487a4f398 100644
--- a/tests/gocase/unit/type/incr/incr_test.go
+++ b/tests/gocase/unit/type/incr/incr_test.go
@@ -31,9 +31,8 @@ import (
func TestIncr(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/type/json/json_test.go
b/tests/gocase/unit/type/json/json_test.go
index a233aaf97..9dddaa539 100644
--- a/tests/gocase/unit/type/json/json_test.go
+++ b/tests/gocase/unit/type/json/json_test.go
@@ -32,9 +32,8 @@ import (
func TestJson(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/type/list/list_test.go
b/tests/gocase/unit/type/list/list_test.go
index 084547883..2fe9f36dd 100644
--- a/tests/gocase/unit/type/list/list_test.go
+++ b/tests/gocase/unit/type/list/list_test.go
@@ -45,9 +45,8 @@ var largeValue = map[string]string{
func TestLTRIM(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
}
@@ -105,9 +104,8 @@ func testLTRIM(t *testing.T, configs
util.KvrocksServerConfigs) {
func TestZipList(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
}
@@ -270,14 +268,12 @@ func testZipList(t *testing.T, configs
util.KvrocksServerConfigs) {
func TestList(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
{
- Name: "resp3-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
},
}
@@ -1576,9 +1572,8 @@ func ExtractKMetadataResponse(result interface{})
(*kMetadataResponse, error) {
func TestRPOPLPUSH(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "resp3-enabled",
- Options: []string{"yes"},
- ConfigType: util.YesNo,
+ Name: "resp3-enabled",
+ Options: []string{"yes"},
},
}
diff --git a/tests/gocase/unit/type/set/set_test.go
b/tests/gocase/unit/type/set/set_test.go
index bb20d4b55..cb6670bfe 100644
--- a/tests/gocase/unit/type/set/set_test.go
+++ b/tests/gocase/unit/type/set/set_test.go
@@ -59,14 +59,12 @@ func GetArrayUnion(arrays ...[]string) []string {
func TestSet(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
{
- Name: "resp3-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/type/sint/sint_test.go
b/tests/gocase/unit/type/sint/sint_test.go
index 4b4f0cb35..0b953b6bc 100644
--- a/tests/gocase/unit/type/sint/sint_test.go
+++ b/tests/gocase/unit/type/sint/sint_test.go
@@ -30,9 +30,8 @@ import (
func TestString(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/type/stream/stream_test.go
b/tests/gocase/unit/type/stream/stream_test.go
index 14d3d361d..df065ebf9 100644
--- a/tests/gocase/unit/type/stream/stream_test.go
+++ b/tests/gocase/unit/type/stream/stream_test.go
@@ -38,14 +38,12 @@ import (
func TestStream(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
{
- Name: "resp3-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/type/strings/strings_test.go
b/tests/gocase/unit/type/strings/strings_test.go
index 3c2515210..8f6648a51 100644
--- a/tests/gocase/unit/type/strings/strings_test.go
+++ b/tests/gocase/unit/type/strings/strings_test.go
@@ -37,9 +37,8 @@ import (
func TestString(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/type/tdigest/tdigest_test.go
b/tests/gocase/unit/type/tdigest/tdigest_test.go
index 6c58d8402..48721b24b 100644
--- a/tests/gocase/unit/type/tdigest/tdigest_test.go
+++ b/tests/gocase/unit/type/tdigest/tdigest_test.go
@@ -77,9 +77,8 @@ func toTdigestInfo(t *testing.T, value interface{})
tdigestInfo {
func TestTDigest(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
}
@@ -839,14 +838,12 @@ func tdigestTests(t *testing.T, configs
util.KvrocksServerConfigs) {
func TestTDigestByRankAndByRevRank(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
{
- Name: "resp3-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/type/timeseries/timeseries_test.go
b/tests/gocase/unit/type/timeseries/timeseries_test.go
index a4763c099..1fd08c2aa 100644
--- a/tests/gocase/unit/type/timeseries/timeseries_test.go
+++ b/tests/gocase/unit/type/timeseries/timeseries_test.go
@@ -34,9 +34,8 @@ import (
func TestTimeSeries(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/unit/type/zset/zset_test.go
b/tests/gocase/unit/type/zset/zset_test.go
index 9d3e125fa..0f9fa6f44 100644
--- a/tests/gocase/unit/type/zset/zset_test.go
+++ b/tests/gocase/unit/type/zset/zset_test.go
@@ -1940,14 +1940,12 @@ func stressTests(t *testing.T, rdb *redis.Client, ctx
context.Context, encoding
func TestZSet(t *testing.T) {
configOptions := []util.ConfigOptions{
{
- Name: "txn-context-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "txn-context-enabled",
+ Options: []string{"yes", "no"},
},
{
- Name: "resp3-enabled",
- Options: []string{"yes", "no"},
- ConfigType: util.YesNo,
+ Name: "resp3-enabled",
+ Options: []string{"yes", "no"},
},
}
diff --git a/tests/gocase/util/configs.go b/tests/gocase/util/configs.go
index c575f25a8..9dc37e354 100644
--- a/tests/gocase/util/configs.go
+++ b/tests/gocase/util/configs.go
@@ -19,35 +19,13 @@
package util
-import "fmt"
-
-type FieldType int
-
-const (
- YesNo FieldType = iota
-)
-
type ConfigOptions struct {
- Name string
- Options []string
- ConfigType FieldType
+ Name string
+ Options []string
}
type KvrocksServerConfigs map[string]string
-func verifyConfigOptions(configType FieldType, option string) error {
- switch configType {
- case YesNo:
- if option == "yes" || option == "no" {
- break
- }
- return fmt.Errorf("invalid option for yes/no config")
- default:
- return fmt.Errorf("unsupported config type")
- }
- return nil
-}
-
// / GenerateConfigsMatrix generates all possible combinations of config
options
func GenerateConfigsMatrix(configOptions []ConfigOptions)
([]KvrocksServerConfigs, error) {
configsMatrix := make([]KvrocksServerConfigs, 0)
@@ -56,27 +34,29 @@ func GenerateConfigsMatrix(configOptions []ConfigOptions)
([]KvrocksServerConfig
helper = func(configs []ConfigOptions, currentIndex int, currentConfig
map[string]string) error {
if currentIndex == len(configOptions) {
- configsMatrix = append(configsMatrix, currentConfig)
+ config := make(KvrocksServerConfigs, len(currentConfig))
+ for k, v := range currentConfig {
+ config[k] = v
+ }
+ configsMatrix = append(configsMatrix, config)
return nil
}
- currentConfigBackup := make(KvrocksServerConfigs,
len(currentConfig))
- for k, v := range currentConfig {
- currentConfigBackup[k] = v
- }
+ configName := configs[currentIndex].Name
+ originalValue, hadOriginalValue := currentConfig[configName]
for _, option := range configs[currentIndex].Options {
- err :=
verifyConfigOptions(configs[currentIndex].ConfigType, option)
+ currentConfig[configName] = option
+ err := helper(configs, currentIndex+1, currentConfig)
if err != nil {
return err
}
+ }
- currentConfig[configs[currentIndex].Name] = option
- err = helper(configs, currentIndex+1, currentConfig)
- if err != nil {
- return err
- }
- currentConfig = currentConfigBackup
+ if hadOriginalValue {
+ currentConfig[configName] = originalValue
+ } else {
+ delete(currentConfig, configName)
}
return nil