This is an automated email from the ASF dual-hosted git repository. PragmaTwice pushed a commit to branch 2.16 in repository https://gitbox.apache.org/repos/asf/kvrocks.git
commit 29b8046dca601d7377668c1a4e0774a4f7bf387a Author: PragmaTwice <[email protected]> AuthorDate: Tue Jun 16 22:59:17 2026 +0800 release: disable HFE feature for 2.16 release --- kvrocks.conf | 20 - src/commands/cmd_hash.cc | 49 +- src/config/config.cc | 5 - tests/cppunit/compact_test.cc | 151 --- tests/cppunit/config_test.cc | 2 - tests/cppunit/types/hash_test.cc | 750 -------------- tests/gocase/unit/type/hash/hash_hfe_test.go | 1350 -------------------------- 7 files changed, 18 insertions(+), 2309 deletions(-) diff --git a/kvrocks.conf b/kvrocks.conf index f1541c496..12b019ca1 100644 --- a/kvrocks.conf +++ b/kvrocks.conf @@ -77,26 +77,6 @@ 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/commands/cmd_hash.cc b/src/commands/cmd_hash.cc index c56aeae63..dbbcfdc6b 100644 --- a/src/commands/cmd_hash.cc +++ b/src/commands/cmd_hash.cc @@ -790,36 +790,23 @@ class CommandHExpireInfo : public Commander { std::vector<std::string> fields_; }; -REDIS_REGISTER_COMMANDS( - Hash, MakeCmdAttr<CommandHGet>("hget", 3, "read-only", 1, 1, 1), - MakeCmdAttr<CommandHIncrBy>("hincrby", 4, "write", 1, 1, 1), - MakeCmdAttr<CommandHIncrByFloat>("hincrbyfloat", 4, "write", 1, 1, 1), - MakeCmdAttr<CommandHMSet>("hset", -4, "write", 1, 1, 1), - MakeCmdAttr<CommandHSetExpire>("hsetexpire", -5, "write", 1, 1, 1), - MakeCmdAttr<CommandHSetNX>("hsetnx", -4, "write", 1, 1, 1), - MakeCmdAttr<CommandHDel>("hdel", -3, "write no-dbsize-check", 1, 1, 1), - MakeCmdAttr<CommandHStrlen>("hstrlen", 3, "read-only", 1, 1, 1), - MakeCmdAttr<CommandHExists>("hexists", 3, "read-only", 1, 1, 1), - MakeCmdAttr<CommandHLen>("hlen", -2, "read-only", 1, 1, 1, GenerateHLenFlags), - MakeCmdAttr<CommandHMGet>("hmget", -3, "read-only", 1, 1, 1), - MakeCmdAttr<CommandHMSet>("hmset", -4, "write", 1, 1, 1), - MakeCmdAttr<CommandHKeys>("hkeys", 2, "read-only slow", 1, 1, 1), - MakeCmdAttr<CommandHVals>("hvals", 2, "read-only slow", 1, 1, 1), - MakeCmdAttr<CommandHGetAll>("hgetall", 2, "read-only slow", 1, 1, 1), - MakeCmdAttr<CommandHScan>("hscan", -3, "read-only", 1, 1, 1), - MakeCmdAttr<CommandHRangeByLex>("hrangebylex", -4, "read-only", 1, 1, 1), - MakeCmdAttr<CommandHRandField>("hrandfield", -2, "read-only slow", 1, 1, 1), - MakeCmdAttr<CommandHExpireGeneric<HashFieldExpireTimeMode::kRelativeSeconds>>("hexpire", -6, "write", 1, 1, 1), - MakeCmdAttr<CommandHExpireGeneric<HashFieldExpireTimeMode::kRelativeMilliseconds>>("hpexpire", -6, "write", 1, 1, - 1), - MakeCmdAttr<CommandHExpireGeneric<HashFieldExpireTimeMode::kAbsoluteSeconds>>("hexpireat", -6, "write", 1, 1, 1), - MakeCmdAttr<CommandHExpireGeneric<HashFieldExpireTimeMode::kAbsoluteMilliseconds>>("hpexpireat", -6, "write", 1, 1, - 1), - MakeCmdAttr<CommandHPersist>("hpersist", -5, "write", 1, 1, 1), - MakeCmdAttr<CommandHExpireInfo<HashFieldExpireTimeMode::kRelativeSeconds>>("httl", -5, "read-only", 1, 1, 1), - MakeCmdAttr<CommandHExpireInfo<HashFieldExpireTimeMode::kRelativeMilliseconds>>("hpttl", -5, "read-only", 1, 1, 1), - MakeCmdAttr<CommandHExpireInfo<HashFieldExpireTimeMode::kAbsoluteSeconds>>("hexpiretime", -5, "read-only", 1, 1, 1), - MakeCmdAttr<CommandHExpireInfo<HashFieldExpireTimeMode::kAbsoluteMilliseconds>>("hpexpiretime", -5, "read-only", 1, - 1, 1), ) +REDIS_REGISTER_COMMANDS(Hash, MakeCmdAttr<CommandHGet>("hget", 3, "read-only", 1, 1, 1), + MakeCmdAttr<CommandHIncrBy>("hincrby", 4, "write", 1, 1, 1), + MakeCmdAttr<CommandHIncrByFloat>("hincrbyfloat", 4, "write", 1, 1, 1), + MakeCmdAttr<CommandHMSet>("hset", -4, "write", 1, 1, 1), + MakeCmdAttr<CommandHSetExpire>("hsetexpire", -5, "write", 1, 1, 1), + MakeCmdAttr<CommandHSetNX>("hsetnx", -4, "write", 1, 1, 1), + MakeCmdAttr<CommandHDel>("hdel", -3, "write no-dbsize-check", 1, 1, 1), + MakeCmdAttr<CommandHStrlen>("hstrlen", 3, "read-only", 1, 1, 1), + MakeCmdAttr<CommandHExists>("hexists", 3, "read-only", 1, 1, 1), + MakeCmdAttr<CommandHLen>("hlen", -2, "read-only", 1, 1, 1, GenerateHLenFlags), + MakeCmdAttr<CommandHMGet>("hmget", -3, "read-only", 1, 1, 1), + MakeCmdAttr<CommandHMSet>("hmset", -4, "write", 1, 1, 1), + MakeCmdAttr<CommandHKeys>("hkeys", 2, "read-only slow", 1, 1, 1), + MakeCmdAttr<CommandHVals>("hvals", 2, "read-only slow", 1, 1, 1), + MakeCmdAttr<CommandHGetAll>("hgetall", 2, "read-only slow", 1, 1, 1), + MakeCmdAttr<CommandHScan>("hscan", -3, "read-only", 1, 1, 1), + MakeCmdAttr<CommandHRangeByLex>("hrangebylex", -4, "read-only", 1, 1, 1), + MakeCmdAttr<CommandHRandField>("hrandfield", -2, "read-only slow", 1, 1, 1), ) } // namespace redis diff --git a/src/config/config.cc b/src/config/config.cc index 492eb975b..6aaaa063b 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -247,11 +247,6 @@ 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/tests/cppunit/compact_test.cc b/tests/cppunit/compact_test.cc index 1d0dc3789..4fd2b77b7 100644 --- a/tests/cppunit/compact_test.cc +++ b/tests/cppunit/compact_test.cc @@ -236,125 +236,6 @@ TEST(Compact, Filter) { } } -TEST_F(CompactHashFieldExpirationTest, DropsExpiredTTLSubkeyWithoutChangingMetadata) { - const std::string key = "hfe_compact_hash"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"persistent", "1"}, {"live", "2"}, {"expired", "3"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 3); - - uint64_t now = util::GetTimeStampMS(); - uint64_t expired_at = now + 200; - uint64_t live_expire_at = now + 60'000; - std::vector<int64_t> results; - s = hash_->ExpireFields(*ctx_, key, {"live"}, live_expire_at, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = hash_->ExpireFields(*ctx_, key, {"expired"}, expired_at, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - - HashMetadata before = hashMetadataOf(key); - ASSERT_EQ(before.size, 3); - ASSERT_EQ(before.persist, 1); - ASSERT_LT(before.lower, before.upper); - usleep(250 * 1000); - ASSERT_LT(expired_at, util::GetTimeStampMS()); - - std::string raw_value; - ASSERT_TRUE(getRawHashValue(key, "persistent", &raw_value).ok()); - ASSERT_TRUE(getRawHashValue(key, "live", &raw_value).ok()); - ASSERT_TRUE(getRawHashValue(key, "expired", &raw_value).ok()); - - engine::SubKeyFilter filter(storage_.get()); - EXPECT_EQ(filter.FilterBlobByKey(0, hashSubKey(key, "expired"), nullptr, nullptr), - rocksdb::CompactionFilter::Decision::kUndetermined); - - compactTwice(); - - EXPECT_TRUE(getRawHashValue(key, "persistent", &raw_value).ok()); - EXPECT_TRUE(getRawHashValue(key, "live", &raw_value).ok()); - EXPECT_TRUE(getRawHashValue(key, "expired", &raw_value).IsNotFound()); - - HashMetadata after = hashMetadataOf(key); - EXPECT_EQ(after.size, before.size); - EXPECT_EQ(after.persist, before.persist); - EXPECT_EQ(after.lower, before.lower); - EXPECT_EQ(after.upper, before.upper); - EXPECT_EQ(after.version, before.version); -} - -TEST_F(CompactHashFieldExpirationTest, DropsWholeHashWhenAllTTLFieldsExpiredByBounds) { - const std::string key = "hfe_compact_whole_hash"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"first", "1"}, {"second", "2"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 2); - - uint64_t expire_at = util::GetTimeStampMS() + 200; - std::vector<int64_t> results; - s = hash_->ExpireFields(*ctx_, key, {"first", "second"}, expire_at, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - - HashMetadata before = hashMetadataOf(key); - ASSERT_TRUE(before.IsFieldExpirationEncoding()); - ASSERT_EQ(before.size, 2); - ASSERT_EQ(before.persist, 0); - ASSERT_EQ(before.lower, expire_at); - ASSERT_EQ(before.upper, expire_at); - - usleep(250 * 1000); - ASSERT_LT(before.upper, util::GetTimeStampMS()); - - std::string raw_value; - std::string first_sub_key = hashSubKey(key, "first"); - std::string second_sub_key = hashSubKey(key, "second"); - ASSERT_TRUE(getRawMetadata(key, &raw_value).ok()); - ASSERT_TRUE(getRawSubKeyValue(first_sub_key, &raw_value).ok()); - ASSERT_TRUE(getRawSubKeyValue(second_sub_key, &raw_value).ok()); - - compactTwice(); - - EXPECT_TRUE(getRawMetadata(key, &raw_value).IsNotFound()); - EXPECT_TRUE(getRawSubKeyValue(first_sub_key, &raw_value).IsNotFound()); - EXPECT_TRUE(getRawSubKeyValue(second_sub_key, &raw_value).IsNotFound()); -} - -TEST_F(CompactHashFieldExpirationTest, KeepsHashMetadataWhenPersistentFieldExistsPastUpperBound) { - const std::string key = "hfe_compact_persistent_survivor"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"persistent", "1"}, {"expired", "2"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 2); - - uint64_t expire_at = util::GetTimeStampMS() + 200; - std::vector<int64_t> results; - s = hash_->ExpireFields(*ctx_, key, {"expired"}, expire_at, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - - HashMetadata before = hashMetadataOf(key); - ASSERT_TRUE(before.IsFieldExpirationEncoding()); - ASSERT_EQ(before.size, 2); - ASSERT_EQ(before.persist, 1); - ASSERT_EQ(before.lower, expire_at); - ASSERT_EQ(before.upper, expire_at); - - usleep(250 * 1000); - ASSERT_LT(before.upper, util::GetTimeStampMS()); - - compactTwice(); - - std::string raw_value; - EXPECT_TRUE(getRawMetadata(key, &raw_value).ok()); - EXPECT_TRUE(getRawHashValue(key, "persistent", &raw_value).ok()); - EXPECT_TRUE(getRawHashValue(key, "expired", &raw_value).IsNotFound()); - - HashMetadata after = hashMetadataOf(key); - EXPECT_EQ(after.size, before.size); - EXPECT_EQ(after.persist, before.persist); - EXPECT_EQ(after.lower, before.lower); - EXPECT_EQ(after.upper, before.upper); - EXPECT_EQ(after.version, before.version); -} - TEST(Compact, KeepsLegacyHashSubkeyWithTTLLikePrefix) { Config config; config.db_dir = "compactdb_legacy_hash_ttl_like_prefix"; @@ -409,38 +290,6 @@ TEST(Compact, KeepsLegacyHashSubkeyWithTTLLikePrefix) { } } -TEST_F(CompactHashFieldExpirationTest, KeepsMalformedFieldExpirationSubkeyValue) { - const std::string key = "hfe_compact_malformed"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"field", "value"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 1); - std::vector<int64_t> results; - s = hash_->ExpireFields(*ctx_, key, {"field"}, util::GetTimeStampMS() + 60'000, HashFieldExpireCondition::kNone, - &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - - HashMetadata before = hashMetadataOf(key); - ASSERT_TRUE(before.IsFieldExpirationEncoding()); - ASSERT_EQ(before.persist, 0); - - s = putRawHashValue(key, "field", "short"); - ASSERT_TRUE(s.ok()) << s.ToString(); - - compactTwice(); - - std::string raw_value; - EXPECT_TRUE(getRawHashValue(key, "field", &raw_value).ok()); - EXPECT_EQ(raw_value, "short"); - - HashMetadata after = hashMetadataOf(key); - EXPECT_EQ(after.size, before.size); - EXPECT_EQ(after.persist, before.persist); - EXPECT_EQ(after.lower, before.lower); - EXPECT_EQ(after.upper, before.upper); - EXPECT_EQ(after.version, before.version); -} - TEST(Compact, SearchFilter) { Config config; config.db_dir = "compactdb"; diff --git a/tests/cppunit/config_test.cc b/tests/cppunit/config_test.cc index aff85b86d..3c78a48f0 100644 --- a/tests/cppunit/config_test.cc +++ b/tests/cppunit/config_test.cc @@ -61,8 +61,6 @@ 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/types/hash_test.cc b/tests/cppunit/types/hash_test.cc index e678aaf5c..0e0926696 100644 --- a/tests/cppunit/types/hash_test.cc +++ b/tests/cppunit/types/hash_test.cc @@ -284,756 +284,6 @@ 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.size, 1); - EXPECT_EQ(metadata.persist, 1); - 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(RedisHashFieldExpirationEncodingTest, PersistentCountTracksPersistentFieldWrites) { - const Slice key = "mode-one-persist-count"; - - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"field-1", "1"}, {"field-2", "2"}}, false, &ret); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(ret, 2); - - HashMetadata metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.mode, HashSubkeyEncodingMode::kFieldExpiration); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 2); - EXPECT_EQ(metadata.lower, 0); - EXPECT_EQ(metadata.upper, 0); - - int64_t new_int = 0; - s = hash_->IncrBy(*ctx_, key, "field-3", 3, &new_int); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(new_int, 3); - - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 3); - EXPECT_EQ(metadata.persist, 3); - - double new_float = 0; - s = hash_->IncrByFloat(*ctx_, key, "field-4", 1.5, &new_float); - ASSERT_TRUE(s.ok()); - EXPECT_DOUBLE_EQ(new_float, 1.5); - - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 4); - EXPECT_EQ(metadata.persist, 4); - - s = hash_->Delete(*ctx_, key, {"field-1", "field-2"}, &ret); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(ret, 2); - - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 2); - EXPECT_EQ(metadata.lower, 0); - EXPECT_EQ(metadata.upper, 0); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, ExpireFieldsMaintainsPersistentToTTLAndTTLToTTLMetadata) { - const Slice key = "hfe-expire-fields-metadata"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"a", "1"}, {"b", "2"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 2); - - HashMetadata metadata = hashMetadata(key.ToString()); - ASSERT_EQ(metadata.size, 2); - ASSERT_EQ(metadata.persist, 2); - ASSERT_EQ(metadata.lower, 0); - ASSERT_EQ(metadata.upper, 0); - - std::vector<int64_t> results; - uint64_t now = util::GetTimeStampMS(); - uint64_t t10 = now + 10'000; - uint64_t t20 = now + 20'000; - uint64_t t5 = now + 5'000; - uint64_t t30 = now + 30'000; - - s = hash_->ExpireFields(*ctx_, key, {"a"}, t10, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()); - ASSERT_EQ(results, std::vector<int64_t>({1})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 1); - EXPECT_EQ(metadata.lower, t10); - EXPECT_EQ(metadata.upper, t10); - - s = hash_->ExpireFields(*ctx_, key, {"b"}, t20, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()); - ASSERT_EQ(results, std::vector<int64_t>({1})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 0); - EXPECT_EQ(metadata.lower, t10); - EXPECT_EQ(metadata.upper, t20); - - s = hash_->ExpireFields(*ctx_, key, {"b"}, t5, HashFieldExpireCondition::kLT, &results); - ASSERT_TRUE(s.ok()); - ASSERT_EQ(results, std::vector<int64_t>({1})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 0); - EXPECT_EQ(metadata.lower, t5); - EXPECT_EQ(metadata.upper, t20); - - s = hash_->ExpireFields(*ctx_, key, {"a"}, t30, HashFieldExpireCondition::kGT, &results); - ASSERT_TRUE(s.ok()); - ASSERT_EQ(results, std::vector<int64_t>({1})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 0); - EXPECT_EQ(metadata.lower, t5); - EXPECT_EQ(metadata.upper, t30); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, PersistFieldsMaintainsTTLToPersistentMetadata) { - const Slice key = "hfe-persist-fields-metadata"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"a", "1"}, {"b", "2"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 2); - - std::vector<int64_t> results; - uint64_t now = util::GetTimeStampMS(); - uint64_t t10 = now + 10'000; - uint64_t t20 = now + 20'000; - s = hash_->ExpireFields(*ctx_, key, {"a"}, t10, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()); - s = hash_->ExpireFields(*ctx_, key, {"b"}, t20, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()); - - HashMetadata metadata = hashMetadata(key.ToString()); - ASSERT_EQ(metadata.size, 2); - ASSERT_EQ(metadata.persist, 0); - ASSERT_EQ(metadata.lower, t10); - ASSERT_EQ(metadata.upper, t20); - - s = hash_->PersistFields(*ctx_, key, {"a"}, &results); - ASSERT_TRUE(s.ok()); - ASSERT_EQ(results, std::vector<int64_t>({1})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 1); - EXPECT_EQ(metadata.lower, t10); - EXPECT_EQ(metadata.upper, t20); - - s = hash_->PersistFields(*ctx_, key, {"b"}, &results); - ASSERT_TRUE(s.ok()); - ASSERT_EQ(results, std::vector<int64_t>({1})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 2); - EXPECT_EQ(metadata.lower, 0); - EXPECT_EQ(metadata.upper, 0); - - s = hash_->PersistFields(*ctx_, key, {"a"}, &results); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(results, std::vector<int64_t>({-1})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 2); - EXPECT_EQ(metadata.lower, 0); - EXPECT_EQ(metadata.upper, 0); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, ExpiredTTLPhysicalIsMissingForReadsAndDoesNotMutateMetadata) { - const Slice key = "hfe-expired-ttl-read"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"a", "1"}, {"b", "2"}, {"c", "3"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 3); - - HashMetadata metadata = hashMetadata(key.ToString()); - metadata.persist = 2; - metadata.lower = util::GetTimeStampMS() - 1000; - metadata.upper = metadata.lower; - s = putHashMetadata(key.ToString(), metadata); - ASSERT_TRUE(s.ok()); - s = putRawHashValue(key.ToString(), "a", metadata.lower, "1"); - ASSERT_TRUE(s.ok()); - - HashMetadata before = hashMetadata(key.ToString()); - std::string got; - s = hash_->Get(*ctx_, key, "a", &got); - EXPECT_TRUE(s.IsNotFound()); - - std::vector<std::string> values; - std::vector<rocksdb::Status> statuses; - s = hash_->MGet(*ctx_, key, {"a", "b"}, &values, &statuses); - ASSERT_TRUE(s.ok()); - EXPECT_TRUE(statuses[0].IsNotFound()); - EXPECT_EQ(values[1], "2"); - - std::vector<FieldValue> field_values; - s = hash_->GetAll(*ctx_, key, &field_values); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(field_values.size(), 2); - - RangeLexSpec spec; - spec.min = "a"; - spec.max = "z"; - spec.count = INT_MAX; - s = hash_->RangeByLex(*ctx_, key, spec, &field_values); - ASSERT_TRUE(s.ok()); - ASSERT_EQ(field_values.size(), 2); - - std::vector<std::string> fields; - s = hash_->Scan(*ctx_, key, "", 10, "", &fields, &values); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(fields.size(), 2); - - HashMetadata after = hashMetadata(key.ToString()); - EXPECT_EQ(after.size, before.size); - EXPECT_EQ(after.persist, before.persist); - EXPECT_EQ(after.lower, before.lower); - EXPECT_EQ(after.upper, before.upper); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, DuplicateFieldsUseCommandLocalState) { - const Slice key = "hfe-duplicate-fields"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"a", "1"}, {"b", "2"}, {"c", "3"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 3); - - std::vector<int64_t> results; - uint64_t future = util::GetTimeStampMS() + 60'000; - s = hash_->ExpireFields(*ctx_, key, {"a", "a"}, future, HashFieldExpireCondition::kNX, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(results, std::vector<int64_t>({1, 0})); - HashMetadata metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 3); - EXPECT_EQ(metadata.persist, 2); - - s = hash_->PersistFields(*ctx_, key, {"a", "a"}, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(results, std::vector<int64_t>({1, -1})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 3); - EXPECT_EQ(metadata.persist, 3); - EXPECT_EQ(metadata.lower, 0); - EXPECT_EQ(metadata.upper, 0); - - s = hash_->ExpireFields(*ctx_, key, {"b", "b"}, util::GetTimeStampMS(), HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(results, std::vector<int64_t>({2, -2})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 2); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, CompactionGhostDoesNotDecrementMetadataOnMissingSubkey) { - const Slice key = "hfe-compaction-ghost"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"field1", "1"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 1); - - std::vector<int64_t> results; - uint64_t future = util::GetTimeStampMS() + 60'000; - s = hash_->ExpireFields(*ctx_, key, {"field1"}, future, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(results, std::vector<int64_t>({1})); - - HashMetadata before = hashMetadata(key.ToString()); - ASSERT_EQ(before.size, 1); - ASSERT_EQ(before.persist, 0); - s = deleteRawHashValue(key.ToString(), "field1"); - ASSERT_TRUE(s.ok()) << s.ToString(); - - s = hash_->PersistFields(*ctx_, key, {"field1"}, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(results, std::vector<int64_t>({-2})); - HashMetadata metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, before.size); - EXPECT_EQ(metadata.persist, before.persist); - EXPECT_EQ(metadata.lower, before.lower); - EXPECT_EQ(metadata.upper, before.upper); - - s = hash_->ExpireFields(*ctx_, key, {"field1"}, future + 10'000, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(results, std::vector<int64_t>({-2})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, before.size); - EXPECT_EQ(metadata.persist, before.persist); - EXPECT_EQ(metadata.lower, before.lower); - EXPECT_EQ(metadata.upper, before.upper); - - s = hash_->Set(*ctx_, key, "field1", "new", &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(ret, 1); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 1); - EXPECT_EQ(metadata.lower, before.lower); - EXPECT_EQ(metadata.upper, before.upper); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, GetFieldsExpireTimeReturnsMissingForMissingKey) { - std::vector<int64_t> results; - auto s = hash_->GetFieldsExpireTime(*ctx_, "hfe-expire-info-missing-key", {"a", "b"}, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(results, (std::vector<int64_t>{-2, -2})); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, GetFieldsExpireTimeCoversPersistentLiveExpiredMissingAndDuplicates) { - const Slice key = "hfe-expire-info-states"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"persist", "1"}, {"live", "2"}, {"expired", "3"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 3); - - std::vector<int64_t> expire_results; - uint64_t now = util::GetTimeStampMS(); - uint64_t live_expire = now + 60'000; - uint64_t expired_rewrite_expire = now + 120'000; - s = hash_->ExpireFields(*ctx_, key, {"live"}, live_expire, HashFieldExpireCondition::kNone, &expire_results, now); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = hash_->ExpireFields(*ctx_, key, {"expired"}, expired_rewrite_expire, HashFieldExpireCondition::kNone, - &expire_results, now); - ASSERT_TRUE(s.ok()) << s.ToString(); - - HashMetadata metadata = hashMetadata(key.ToString()); - ASSERT_EQ(metadata.size, 3); - ASSERT_EQ(metadata.persist, 1); - uint64_t expired_at = now - 1; - s = putRawHashValue(key.ToString(), "expired", expired_at, "3"); - ASSERT_TRUE(s.ok()) << s.ToString(); - metadata.lower = expired_at; - s = putHashMetadata(key.ToString(), metadata); - ASSERT_TRUE(s.ok()) << s.ToString(); - HashMetadata before = hashMetadata(key.ToString()); - - std::vector<int64_t> results; - s = hash_->GetFieldsExpireTime(*ctx_, key, {"persist", "live", "expired", "missing", "live"}, &results, now); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(results, - (std::vector<int64_t>{-1, static_cast<int64_t>(live_expire), -2, -2, static_cast<int64_t>(live_expire)})); - - HashMetadata after = hashMetadata(key.ToString()); - EXPECT_EQ(after.size, before.size); - EXPECT_EQ(after.persist, before.persist); - EXPECT_EQ(after.lower, before.lower); - EXPECT_EQ(after.upper, before.upper); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, GetFieldsExpireTimeReturnsAbsoluteMilliseconds) { - const Slice key = "hfe-expire-info-format"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"field", "1"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 1); - - uint64_t expire_at = util::GetTimeStampMS() + 60'123; - std::vector<int64_t> expire_results; - s = hash_->ExpireFields(*ctx_, key, {"field"}, expire_at, HashFieldExpireCondition::kNone, &expire_results); - ASSERT_TRUE(s.ok()) << s.ToString(); - - std::vector<int64_t> results; - s = hash_->GetFieldsExpireTime(*ctx_, key, {"field"}, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(results, (std::vector<int64_t>{static_cast<int64_t>(expire_at)})); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, GetFieldsExpireTimeDoesNotRepairCompactionGhost) { - const Slice key = "hfe-expire-info-ghost"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"ghost", "1"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 1); - - std::vector<int64_t> expire_results; - uint64_t future = util::GetTimeStampMS() + 60'000; - s = hash_->ExpireFields(*ctx_, key, {"ghost"}, future, HashFieldExpireCondition::kNone, &expire_results); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(expire_results, std::vector<int64_t>({1})); - - HashMetadata before = hashMetadata(key.ToString()); - ASSERT_EQ(before.size, 1); - ASSERT_EQ(before.persist, 0); - s = deleteRawHashValue(key.ToString(), "ghost"); - ASSERT_TRUE(s.ok()) << s.ToString(); - - std::vector<int64_t> results; - s = hash_->GetFieldsExpireTime(*ctx_, key, {"ghost"}, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(results, (std::vector<int64_t>{-2})); - - HashMetadata after = hashMetadata(key.ToString()); - EXPECT_EQ(after.size, before.size); - EXPECT_EQ(after.persist, before.persist); - EXPECT_EQ(after.lower, before.lower); - EXPECT_EQ(after.upper, before.upper); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, SizeRepairsExpiredPhysicalAndGhostMetadata) { - const Slice key = "hfe-size-repair"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"persistent", "1"}, {"live", "2"}, {"expired", "3"}, {"ghost", "4"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 4); - - std::vector<int64_t> results; - uint64_t now = util::GetTimeStampMS(); - uint64_t live_expire = now + 60'000; - uint64_t expired_at = now - 1; - s = hash_->ExpireFields(*ctx_, key, {"live"}, live_expire, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = hash_->ExpireFields(*ctx_, key, {"ghost"}, live_expire + 60'000, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - - HashMetadata metadata = hashMetadata(key.ToString()); - s = putRawHashValue(key.ToString(), "expired", expired_at, "3"); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = deleteRawHashValue(key.ToString(), "ghost"); - ASSERT_TRUE(s.ok()) << s.ToString(); - - metadata.size = 4; - metadata.persist = 1; - metadata.lower = expired_at; - metadata.upper = live_expire + 60'000; - s = putHashMetadata(key.ToString(), metadata); - ASSERT_TRUE(s.ok()) << s.ToString(); - - s = hash_->Size(*ctx_, key, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(ret, 2); - - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 2); - EXPECT_EQ(metadata.persist, 1); - EXPECT_EQ(metadata.lower, live_expire); - EXPECT_EQ(metadata.upper, live_expire); - - std::string value; - s = hash_->Get(*ctx_, key, "expired", &value); - EXPECT_TRUE(s.IsNotFound()); - s = hash_->Get(*ctx_, key, "ghost", &value); - EXPECT_TRUE(s.IsNotFound()); - s = hash_->Get(*ctx_, key, "persistent", &value); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(value, "1"); - s = hash_->Get(*ctx_, key, "live", &value); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(value, "2"); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, SizeDeletesHashWhenAllTtlCandidatesExpired) { - const Slice key = "hfe-size-delete-all-expired"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"a", "1"}, {"b", "2"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 2); - - uint64_t now = util::GetTimeStampMS(); - uint64_t lower = now - 2'000; - uint64_t upper = now - 1'000; - s = putRawHashValue(key.ToString(), "a", lower, "1"); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = putRawHashValue(key.ToString(), "b", upper, "2"); - ASSERT_TRUE(s.ok()) << s.ToString(); - - HashMetadata metadata = hashMetadata(key.ToString()); - metadata.size = 2; - metadata.persist = 0; - metadata.lower = lower; - metadata.upper = upper; - s = putHashMetadata(key.ToString(), metadata); - ASSERT_TRUE(s.ok()) << s.ToString(); - - s = hash_->Size(*ctx_, key, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(ret, 0); - - metadata = HashMetadata(false); - s = getHashMetadata(key.ToString(), &metadata); - EXPECT_TRUE(s.IsNotFound()); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, DeleteHandlesPersistentLiveExpiredMissingAndDuplicateFields) { - const Slice key = "hfe-delete-state-matrix"; - uint64_t ret = 0; - auto s = - hash_->MSet(*ctx_, key, {{"persistent", "1"}, {"live", "2"}, {"expired", "3"}, {"keeper", "4"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 4); - - std::vector<int64_t> results; - uint64_t now = util::GetTimeStampMS(); - s = hash_->ExpireFields(*ctx_, key, {"live"}, now + 60'000, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = hash_->ExpireFields(*ctx_, key, {"expired"}, now, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(results, std::vector<int64_t>({2})); - - HashMetadata metadata = hashMetadata(key.ToString()); - ASSERT_EQ(metadata.size, 3); - ASSERT_EQ(metadata.persist, 2); - - s = putRawHashValue(key.ToString(), "expired", now - 1, "3"); - ASSERT_TRUE(s.ok()) << s.ToString(); - metadata.size = 4; - metadata.persist = 2; - metadata.lower = now - 1; - metadata.upper = now + 60'000; - s = putHashMetadata(key.ToString(), metadata); - ASSERT_TRUE(s.ok()) << s.ToString(); - - s = hash_->Delete(*ctx_, key, {"persistent", "live", "expired", "missing", "persistent"}, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(ret, 2); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 1); - EXPECT_EQ(metadata.persist, 1); - EXPECT_EQ(metadata.lower, 0); - EXPECT_EQ(metadata.upper, 0); - - std::vector<FieldValue> fields; - s = hash_->GetAll(*ctx_, key, &fields); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(fields.size(), 1); - EXPECT_EQ(fields[0].field, "keeper"); - EXPECT_EQ(fields[0].value, "4"); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, MSetHandlesPersistentLiveExpiredAndGhostFields) { - const Slice key = "hfe-mset-state-matrix"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"persistent", "1"}, {"live", "2"}, {"expired", "3"}, {"ghost", "4"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 4); - - std::vector<int64_t> results; - uint64_t now = util::GetTimeStampMS(); - s = hash_->ExpireFields(*ctx_, key, {"live"}, now + 60'000, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = hash_->ExpireFields(*ctx_, key, {"expired"}, now, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = hash_->ExpireFields(*ctx_, key, {"ghost"}, now + 120'000, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()) << s.ToString(); - - HashMetadata before = hashMetadata(key.ToString()); - ASSERT_EQ(before.size, 3); - ASSERT_EQ(before.persist, 1); - s = putRawHashValue(key.ToString(), "expired", now - 1, "3"); - ASSERT_TRUE(s.ok()) << s.ToString(); - before.size = 4; - before.lower = now - 1; - before.upper = now + 120'000; - s = putHashMetadata(key.ToString(), before); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = deleteRawHashValue(key.ToString(), "ghost"); - ASSERT_TRUE(s.ok()) << s.ToString(); - - s = hash_->MSet(*ctx_, key, - {{"persistent", "11"}, {"live", "22"}, {"expired", "33"}, {"ghost", "44"}, {"missing", "55"}}, false, - &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(ret, 3); - HashMetadata metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 6); - EXPECT_EQ(metadata.persist, 5); - EXPECT_EQ(metadata.lower, before.lower); - EXPECT_EQ(metadata.upper, before.upper); - - std::string value; - s = hash_->Get(*ctx_, key, "persistent", &value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(value, "11"); - s = hash_->Get(*ctx_, key, "live", &value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(value, "22"); - s = hash_->Get(*ctx_, key, "expired", &value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(value, "33"); - s = hash_->Get(*ctx_, key, "ghost", &value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(value, "44"); - s = hash_->Get(*ctx_, key, "missing", &value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(value, "55"); - - s = hash_->PersistFields(*ctx_, key, {"ghost"}, &results); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(results, std::vector<int64_t>({-1})); - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 6); - EXPECT_EQ(metadata.persist, 5); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, SetNXHandlesPersistentLiveExpiredAndGhostFields) { - const Slice key = "hfe-msetnx-state-matrix"; - uint64_t ret = 0; - auto s = hash_->MSet(*ctx_, key, {{"persistent", "1"}, {"live", "2"}, {"expired", "3"}, {"ghost", "4"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 4); - - std::vector<int64_t> results; - uint64_t now = util::GetTimeStampMS(); - s = hash_->ExpireFields(*ctx_, key, {"live"}, now + 60'000, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()); - s = hash_->ExpireFields(*ctx_, key, {"expired"}, now, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()); - s = hash_->ExpireFields(*ctx_, key, {"ghost"}, now + 120'000, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()); - - HashMetadata before = hashMetadata(key.ToString()); - s = putRawHashValue(key.ToString(), "expired", now - 1, "3"); - ASSERT_TRUE(s.ok()) << s.ToString(); - before.size = 4; - before.lower = now - 1; - before.upper = now + 120'000; - s = putHashMetadata(key.ToString(), before); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = deleteRawHashValue(key.ToString(), "ghost"); - ASSERT_TRUE(s.ok()) << s.ToString(); - - s = hash_->MSet(*ctx_, key, - {{"persistent", "11"}, {"live", "22"}, {"expired", "33"}, {"ghost", "44"}, {"missing", "55"}}, true, - &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - EXPECT_EQ(ret, 3); - HashMetadata metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 6); - EXPECT_EQ(metadata.persist, 4); - EXPECT_EQ(metadata.lower, before.lower); - EXPECT_EQ(metadata.upper, before.upper); - - std::string value; - s = hash_->Get(*ctx_, key, "persistent", &value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(value, "1"); - s = hash_->Get(*ctx_, key, "live", &value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(value, "2"); - s = hash_->Get(*ctx_, key, "expired", &value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(value, "33"); - s = hash_->Get(*ctx_, key, "ghost", &value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(value, "44"); - s = hash_->Get(*ctx_, key, "missing", &value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(value, "55"); -} - -TEST_F(RedisHashFieldExpirationEncodingTest, IncrementsKeepLiveTTLAndTreatExpiredPhysicalAndGhostAsZero) { - const Slice key = "hfe-incr-state-matrix"; - uint64_t ret = 0; - auto s = - hash_->MSet(*ctx_, key, {{"persistent", "10"}, {"live", "20"}, {"expired", "30"}, {"ghost", "40"}}, false, &ret); - ASSERT_TRUE(s.ok()) << s.ToString(); - ASSERT_EQ(ret, 4); - - std::vector<int64_t> results; - uint64_t now = util::GetTimeStampMS(); - s = hash_->ExpireFields(*ctx_, key, {"live"}, now + 60'000, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()); - s = hash_->ExpireFields(*ctx_, key, {"expired"}, now, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()); - s = hash_->ExpireFields(*ctx_, key, {"ghost"}, now + 120'000, HashFieldExpireCondition::kNone, &results); - ASSERT_TRUE(s.ok()); - - HashMetadata metadata = hashMetadata(key.ToString()); - s = putRawHashValue(key.ToString(), "expired", now - 1, "30"); - ASSERT_TRUE(s.ok()) << s.ToString(); - metadata.size = 4; - metadata.lower = now - 1; - metadata.upper = now + 120'000; - s = putHashMetadata(key.ToString(), metadata); - ASSERT_TRUE(s.ok()) << s.ToString(); - s = deleteRawHashValue(key.ToString(), "ghost"); - ASSERT_TRUE(s.ok()) << s.ToString(); - - int64_t int_value = 0; - s = hash_->IncrBy(*ctx_, key, "persistent", 1, &int_value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(int_value, 11); - s = hash_->IncrBy(*ctx_, key, "live", 1, &int_value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(int_value, 21); - metadata = hashMetadata(key.ToString()); - std::string raw_value = rawHashValue(key.ToString(), "live", &metadata); - Slice decoded_value(raw_value); - uint64_t live_expire = 0; - ASSERT_TRUE(metadata.DecodeSubkeyValue(&decoded_value, &live_expire).ok()); - EXPECT_EQ(decoded_value.ToStringView(), "21"); - EXPECT_EQ(live_expire, now + 60'000); - s = hash_->IncrBy(*ctx_, key, "expired", 1, &int_value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(int_value, 1); - s = hash_->IncrBy(*ctx_, key, "ghost", 1, &int_value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(int_value, 1); - s = hash_->IncrBy(*ctx_, key, "missing", 1, &int_value); - ASSERT_TRUE(s.ok()); - EXPECT_EQ(int_value, 1); - - metadata = hashMetadata(key.ToString()); - EXPECT_EQ(metadata.size, 6); - EXPECT_EQ(metadata.persist, 4); - EXPECT_EQ(metadata.lower, now - 1); - EXPECT_EQ(metadata.upper, now + 120'000); - - double float_value = 0; - s = hash_->IncrByFloat(*ctx_, key, "live", 0.5, &float_value); - ASSERT_TRUE(s.ok()); - EXPECT_DOUBLE_EQ(float_value, 21.5); - metadata = hashMetadata(key.ToString()); - raw_value = rawHashValue(key.ToString(), "live", &metadata); - decoded_value = Slice(raw_value); - live_expire = 0; - ASSERT_TRUE(metadata.DecodeSubkeyValue(&decoded_value, &live_expire).ok()); - EXPECT_EQ(decoded_value.ToStringView(), "21.5"); - EXPECT_EQ(live_expire, now + 60'000); - EXPECT_EQ(metadata.size, 6); - EXPECT_EQ(metadata.persist, 4); - EXPECT_EQ(metadata.lower, now - 1); - EXPECT_EQ(metadata.upper, now + 120'000); -} - TEST_F(RedisHashTest, HIncr) { int64_t value = 0; Slice field("hash-incrby-invalid-field"); diff --git a/tests/gocase/unit/type/hash/hash_hfe_test.go b/tests/gocase/unit/type/hash/hash_hfe_test.go deleted file mode 100644 index cc12d30a6..000000000 --- a/tests/gocase/unit/type/hash/hash_hfe_test.go +++ /dev/null @@ -1,1350 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package hash - -import ( - "context" - "errors" - "sort" - "testing" - "time" - - "github.com/redis/go-redis/v9" - "github.com/stretchr/testify/require" - - "github.com/apache/kvrocks/tests/gocase/util" -) - -const ( - hfePersistentField = "a-persistent" - hfeLiveField = "b-live" - hfeExpiredField = "c-expired" - hfeMissingField = "d-missing" - hfeKeeperField = "z-keeper" - hfeLiveTTLSeconds = 300 -) - -const hfeMaxAbsTimeMs = int64(1<<46 - 1) - -func runWithFieldExpirationHash(t *testing.T, fn func(t *testing.T, rdb *redis.Client, ctx context.Context)) { - t.Helper() - - runWithFieldExpirationHashConfigs(t, nil, fn) -} - -func runWithFieldExpirationHashConfigs(t *testing.T, configs util.KvrocksServerConfigs, fn func(t *testing.T, rdb *redis.Client, ctx context.Context)) { - t.Helper() - - serverConfigs := util.KvrocksServerConfigs{ - "hash-encoding-mode": "field-expiration", - "resp3-enabled": "yes", - } - for k, v := range configs { - serverConfigs[k] = v - } - srv := util.StartServer(t, serverConfigs) - defer srv.Close() - - ctx := context.Background() - rdb := srv.NewClient() - defer func() { require.NoError(t, rdb.Close()) }() - fn(t, rdb, ctx) -} - -func requireHashMetadata(t *testing.T, meta util.KMetadataResponse, size, persist int64) { - t.Helper() - - require.Equal(t, "hash", meta.Type) - require.Equal(t, "field-expiration", meta.Mode) - require.Equal(t, size, meta.Size) - require.Equal(t, persist, meta.Persist) - require.LessOrEqual(t, meta.Persist, meta.Size) - if meta.Size == meta.Persist { - require.Equal(t, int64(0), meta.Lower) - require.Equal(t, int64(0), meta.Upper) - } else { - require.Greater(t, meta.Lower, int64(0)) - require.GreaterOrEqual(t, meta.Upper, meta.Lower) - } -} - -func requireHLenCommandInfoFlags(t *testing.T, rdb *redis.Client, ctx context.Context, want []interface{}) { - t.Helper() - - info, err := rdb.Do(ctx, "command", "info", "hlen").Slice() - require.NoError(t, err) - require.Len(t, info, 1) - hlenInfo := info[0].([]interface{}) - require.Len(t, hlenInfo, 6) - require.Equal(t, "hlen", hlenInfo[0]) - require.Equal(t, want, hlenInfo[2]) -} - -func requireCommandInfo(t *testing.T, rdb *redis.Client, ctx context.Context, command string, arity int64, readonly bool) { - t.Helper() - - info, err := rdb.Do(ctx, "command", "info", command).Slice() - require.NoError(t, err) - require.Len(t, info, 1) - commandInfo := info[0].([]interface{}) - require.Len(t, commandInfo, 6) - require.Equal(t, command, commandInfo[0]) - require.Equal(t, arity, commandInfo[1]) - - flags := commandInfo[2].([]interface{}) - if readonly { - require.Contains(t, flags, "readonly") - } else { - require.NotContains(t, flags, "readonly") - } -} - -func waitHashFieldExpired(t *testing.T, rdb *redis.Client, ctx context.Context, key, field string) { - t.Helper() - - require.Eventually(t, func() bool { - err := rdb.HGet(ctx, key, field).Err() - return errors.Is(err, redis.Nil) - }, 5*time.Second, 50*time.Millisecond) -} - -func requireIntArray(t *testing.T, got interface{}, want []int64) { - t.Helper() - - items, ok := got.([]interface{}) - require.Truef(t, ok, "expected []interface{}, got %T", got) - require.Len(t, items, len(want)) - for i, item := range items { - require.Equal(t, want[i], item) - } -} - -func createHashFieldStates(t *testing.T, rdb *redis.Client, ctx context.Context, key string) { - t.Helper() - - require.Equal(t, int64(4), rdb.HSet(ctx, key, - hfePersistentField, "10", - hfeLiveField, "20", - hfeExpiredField, "30", - hfeKeeperField, "40").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, hfeLiveTTLSeconds, "FIELDS", 1, hfeLiveField).Val(), []int64{1}) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 1, hfeExpiredField).Val(), []int64{1}) - waitHashFieldExpired(t, rdb, ctx, key, hfeExpiredField) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 4, 2) -} - -func scanPairsToMap(t *testing.T, pairs []string) map[string]string { - t.Helper() - - require.Equal(t, 0, len(pairs)%2) - result := make(map[string]string, len(pairs)/2) - for i := 0; i < len(pairs); i += 2 { - result[pairs[i]] = pairs[i+1] - } - return result -} - -func requireHashValues(t *testing.T, rdb *redis.Client, ctx context.Context, key string, want map[string]string) { - t.Helper() - - for field, value := range want { - require.Equal(t, value, rdb.HGet(ctx, key, field).Val(), field) - } -} - -func futureUnixTimes(after time.Duration) (int64, int64) { - expireAt := time.Now().Add(after) - return expireAt.Unix(), expireAt.UnixMilli() -} - -func expireCommandArgs(command, key string, ttl time.Duration, extra ...interface{}) []interface{} { - args := []interface{}{command, key} - switch command { - case "hexpire": - args = append(args, int64(ttl/time.Second)) - case "hpexpire": - args = append(args, int64(ttl/time.Millisecond)) - case "hexpireat": - sec, _ := futureUnixTimes(ttl) - args = append(args, sec) - case "hpexpireat": - _, ms := futureUnixTimes(ttl) - args = append(args, ms) - default: - panic("unknown HFE expire command") - } - return append(args, extra...) -} - -func TestHashFieldExpirationMetadataLifecycle(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-lifecycle" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "a", "1", "b", "2").Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 2) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 60, "FIELDS", 1, "a").Val(), []int64{1}) - m1 := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, m1, 2, 1) - require.Equal(t, m1.Lower, m1.Upper) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 120, "FIELDS", 1, "b").Val(), []int64{1}) - m2 := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, m2, 2, 0) - require.Equal(t, m1.Lower, m2.Lower) - require.Greater(t, m2.Upper, m1.Upper) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 30, "LT", "FIELDS", 1, "b").Val(), []int64{1}) - m3 := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, m3, 2, 0) - require.Less(t, m3.Lower, m2.Lower) - require.Equal(t, m2.Upper, m3.Upper) - - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 1, "b").Val(), []int64{1}) - m4 := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, m4, 2, 1) - require.Equal(t, m3.Lower, m4.Lower) - require.Equal(t, m3.Upper, m4.Upper) - - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 1, "a").Val(), []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 2) - }) -} - -func TestHashFieldExpirationFiltersReadsWithoutMutatingMetadata(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-read-filter" - require.Equal(t, int64(3), rdb.HSet(ctx, key, "a", "1", "b", "2", "c", "3").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 1, "a").Val(), []int64{1}) - before := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, before, 3, 2) - waitHashFieldExpired(t, rdb, ctx, key, "a") - - require.ErrorIs(t, rdb.HGet(ctx, key, "a").Err(), redis.Nil) - require.False(t, rdb.HExists(ctx, key, "a").Val()) - require.Equal(t, int64(0), rdb.HStrLen(ctx, key, "a").Val()) - require.Equal(t, []interface{}{nil, "2"}, rdb.HMGet(ctx, key, "a", "b").Val()) - - all := rdb.HGetAll(ctx, key).Val() - require.NotContains(t, all, "a") - keys := rdb.HKeys(ctx, key).Val() - require.NotContains(t, keys, "a") - values := rdb.HVals(ctx, key).Val() - require.ElementsMatch(t, []string{"2", "3"}, values) - scanned, _, err := rdb.HScan(ctx, key, 0, "", 10).Result() - require.NoError(t, err) - require.NotContains(t, scanned, "a") - scanned, cursor, err := rdb.HScan(ctx, key, 0, "", 1).Result() - require.NoError(t, err) - require.Equal(t, []string{"b", "2"}, scanned) - require.NotZero(t, cursor) - rangeByLex := rdb.Do(ctx, "hrangebylex", key, "[a", "[zz", "LIMIT", 0, 10).Val() - require.NotContains(t, rangeByLex, "a") - randField := rdb.HRandField(ctx, key, 10).Val() - require.NotContains(t, randField, "a") - after := util.GetKMetadata(t, rdb, ctx, key) - require.Equal(t, before, after) - }) -} - -func TestHashFieldExpirationWriteCleanupMetadata(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - makeExpired := func(t *testing.T, key, value string) { - t.Helper() - require.Equal(t, int64(2), rdb.HSet(ctx, key, "a", value, "b", "2").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 1, "a").Val(), []int64{1}) - waitHashFieldExpired(t, rdb, ctx, key, "a") - } - - t.Run("hdel", func(t *testing.T) { - key := "hfe-cleanup-hdel" - makeExpired(t, key, "1") - require.Equal(t, int64(0), rdb.HDel(ctx, key, "a").Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 1, 1) - }) - - t.Run("hpersist", func(t *testing.T) { - key := "hfe-cleanup-hpersist" - makeExpired(t, key, "1") - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 1, "a").Val(), []int64{-2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 1, 1) - }) - - t.Run("hexpire", func(t *testing.T) { - key := "hfe-cleanup-hexpire" - makeExpired(t, key, "1") - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 60, "FIELDS", 1, "a").Val(), []int64{-2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 1, 1) - }) - - t.Run("hset", func(t *testing.T) { - key := "hfe-cleanup-hset" - makeExpired(t, key, "1") - require.Equal(t, int64(1), rdb.HSet(ctx, key, "a", "new").Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 2) - require.Equal(t, "new", rdb.HGet(ctx, key, "a").Val()) - }) - - t.Run("hsetnx", func(t *testing.T) { - key := "hfe-cleanup-hsetnx" - makeExpired(t, key, "1") - require.Equal(t, true, rdb.HSetNX(ctx, key, "a", "new").Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 2) - }) - - t.Run("hincrby", func(t *testing.T) { - key := "hfe-cleanup-hincrby" - makeExpired(t, key, "bad") - require.Equal(t, int64(2), rdb.HIncrBy(ctx, key, "a", 2).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 2) - }) - - t.Run("hincrbyfloat", func(t *testing.T) { - key := "hfe-cleanup-hincrbyfloat" - makeExpired(t, key, "bad") - require.Equal(t, 1.5, rdb.HIncrByFloat(ctx, key, "a", 1.5).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 2) - }) - }) -} - -func TestHashFieldExpirationReadCommandsAcrossFieldStates(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-read-state-matrix" - createHashFieldStates(t, rdb, ctx, key) - before := util.GetKMetadata(t, rdb, ctx, key) - - require.Equal(t, "10", rdb.HGet(ctx, key, hfePersistentField).Val()) - require.Equal(t, "20", rdb.HGet(ctx, key, hfeLiveField).Val()) - require.ErrorIs(t, rdb.HGet(ctx, key, hfeExpiredField).Err(), redis.Nil) - require.ErrorIs(t, rdb.HGet(ctx, key, hfeMissingField).Err(), redis.Nil) - - require.True(t, rdb.HExists(ctx, key, hfePersistentField).Val()) - require.True(t, rdb.HExists(ctx, key, hfeLiveField).Val()) - require.False(t, rdb.HExists(ctx, key, hfeExpiredField).Val()) - require.False(t, rdb.HExists(ctx, key, hfeMissingField).Val()) - - require.Equal(t, int64(2), rdb.HStrLen(ctx, key, hfePersistentField).Val()) - require.Equal(t, int64(2), rdb.HStrLen(ctx, key, hfeLiveField).Val()) - require.Equal(t, int64(0), rdb.HStrLen(ctx, key, hfeExpiredField).Val()) - require.Equal(t, int64(0), rdb.HStrLen(ctx, key, hfeMissingField).Val()) - - require.Equal(t, []interface{}{"10", "20", nil, nil}, - rdb.HMGet(ctx, key, hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField).Val()) - - require.Equal(t, map[string]string{ - hfePersistentField: "10", - hfeLiveField: "20", - hfeKeeperField: "40", - }, rdb.HGetAll(ctx, key).Val()) - require.ElementsMatch(t, []string{hfePersistentField, hfeLiveField, hfeKeeperField}, rdb.HKeys(ctx, key).Val()) - require.ElementsMatch(t, []string{"10", "20", "40"}, rdb.HVals(ctx, key).Val()) - - scanned, cursor, err := rdb.HScan(ctx, key, 0, "", 100).Result() - require.NoError(t, err) - require.Zero(t, cursor) - require.Equal(t, map[string]string{ - hfePersistentField: "10", - hfeLiveField: "20", - hfeKeeperField: "40", - }, scanPairsToMap(t, scanned)) - - scannedKeys, cursor, err := rdb.HScanNoValues(ctx, key, 0, "", 100).Result() - require.NoError(t, err) - require.Zero(t, cursor) - require.ElementsMatch(t, []string{hfePersistentField, hfeLiveField, hfeKeeperField}, scannedKeys) - - rangeByLex := rdb.Do(ctx, "hrangebylex", key, "[a", "[zz", "LIMIT", 0, 10).Val() - require.Equal(t, []interface{}{ - hfePersistentField, "10", - hfeLiveField, "20", - hfeKeeperField, "40", - }, rangeByLex) - require.Equal(t, []interface{}{hfeLiveField, "20"}, - rdb.Do(ctx, "hrangebylex", key, "[a", "[zz", "LIMIT", 1, 1).Val()) - require.Equal(t, []interface{}{ - hfeKeeperField, "40", - hfeLiveField, "20", - hfePersistentField, "10", - }, rdb.Do(ctx, "hrangebylex", key, "[zz", "[a", "REV", "LIMIT", 0, 10).Val()) - - randFields := rdb.HRandField(ctx, key, 20).Val() - require.ElementsMatch(t, []string{hfePersistentField, hfeLiveField, hfeKeeperField}, randFields) - randFields = rdb.HRandField(ctx, key, -20).Val() - require.NotContains(t, randFields, hfeExpiredField) - require.NotContains(t, randFields, hfeMissingField) - for _, field := range randFields { - require.Contains(t, []string{hfePersistentField, hfeLiveField, hfeKeeperField}, field) - } - randWithValues := rdb.HRandFieldWithValues(ctx, key, 20).Val() - gotRandValues := map[string]string{} - for _, kv := range randWithValues { - gotRandValues[kv.Key] = kv.Value - } - require.Equal(t, map[string]string{ - hfePersistentField: "10", - hfeLiveField: "20", - hfeKeeperField: "40", - }, gotRandValues) - - after := util.GetKMetadata(t, rdb, ctx, key) - require.Equal(t, before, after) - }) -} - -func TestHashFieldExpirationWriteCommandsAcrossFieldStates(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - t.Run("hdel mixed fields", func(t *testing.T) { - key := "hfe-write-hdel-mixed" - createHashFieldStates(t, rdb, ctx, key) - require.Equal(t, int64(2), - rdb.HDel(ctx, key, hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField, hfePersistentField).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 1, 1) - require.Equal(t, map[string]string{hfeKeeperField: "40"}, rdb.HGetAll(ctx, key).Val()) - }) - - t.Run("hset clears ttl and treats expired as new", func(t *testing.T) { - key := "hfe-write-hset-mixed" - createHashFieldStates(t, rdb, ctx, key) - require.Equal(t, int64(2), rdb.HSet(ctx, key, - hfePersistentField, "11", - hfeLiveField, "21", - hfeExpiredField, "31", - hfeMissingField, "41").Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 5) - requireHashValues(t, rdb, ctx, key, map[string]string{ - hfePersistentField: "11", - hfeLiveField: "21", - hfeExpiredField: "31", - hfeMissingField: "41", - hfeKeeperField: "40", - }) - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 4, - hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField).Val(), []int64{-1, -1, -1, -1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 5) - }) - - t.Run("hmset clears ttl and returns ok", func(t *testing.T) { - key := "hfe-write-hmset-mixed" - createHashFieldStates(t, rdb, ctx, key) - require.True(t, rdb.HMSet(ctx, key, - hfePersistentField, "11", - hfeLiveField, "21", - hfeExpiredField, "31", - hfeMissingField, "41").Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 5) - requireHashValues(t, rdb, ctx, key, map[string]string{ - hfePersistentField: "11", - hfeLiveField: "21", - hfeExpiredField: "31", - hfeMissingField: "41", - hfeKeeperField: "40", - }) - }) - - t.Run("hsetnx writes only missing and expired fields", func(t *testing.T) { - key := "hfe-write-hsetnx-mixed" - createHashFieldStates(t, rdb, ctx, key) - require.Equal(t, int64(2), rdb.Do(ctx, "hsetnx", key, - hfePersistentField, "11", - hfeLiveField, "21", - hfeExpiredField, "31", - hfeMissingField, "41").Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 4) - require.Equal(t, "10", rdb.HGet(ctx, key, hfePersistentField).Val()) - require.Equal(t, "20", rdb.HGet(ctx, key, hfeLiveField).Val()) - require.Equal(t, "31", rdb.HGet(ctx, key, hfeExpiredField).Val()) - require.Equal(t, "41", rdb.HGet(ctx, key, hfeMissingField).Val()) - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 1, hfeLiveField).Val(), []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 5) - }) - - t.Run("hincrby keeps live ttl and ignores expired value", func(t *testing.T) { - key := "hfe-write-hincrby-mixed" - createHashFieldStates(t, rdb, ctx, key) - require.Equal(t, int64(15), rdb.HIncrBy(ctx, key, hfePersistentField, 5).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 4, 2) - require.Equal(t, int64(25), rdb.HIncrBy(ctx, key, hfeLiveField, 5).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 4, 2) - require.Equal(t, int64(5), rdb.HIncrBy(ctx, key, hfeExpiredField, 5).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 4, 3) - require.Equal(t, int64(5), rdb.HIncrBy(ctx, key, hfeMissingField, 5).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 4) - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 1, hfeLiveField).Val(), []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 5) - }) - - t.Run("hincrbyfloat keeps live ttl and ignores expired value", func(t *testing.T) { - key := "hfe-write-hincrbyfloat-mixed" - createHashFieldStates(t, rdb, ctx, key) - require.Equal(t, 10.5, rdb.HIncrByFloat(ctx, key, hfePersistentField, 0.5).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 4, 2) - require.Equal(t, 20.5, rdb.HIncrByFloat(ctx, key, hfeLiveField, 0.5).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 4, 2) - require.Equal(t, 0.5, rdb.HIncrByFloat(ctx, key, hfeExpiredField, 0.5).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 4, 3) - require.Equal(t, 0.5, rdb.HIncrByFloat(ctx, key, hfeMissingField, 0.5).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 4) - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 1, hfeLiveField).Val(), []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 5) - }) - }) -} - -func TestHashFieldExpirationSetExpireAcrossFieldStates(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-hsetexpire-mixed" - createHashFieldStates(t, rdb, ctx, key) - - require.Equal(t, "OK", rdb.Do(ctx, "hsetexpire", key, 60, - hfePersistentField, "11", - hfeLiveField, "21", - hfeExpiredField, "31", - hfeMissingField, "41").Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 5) - requireHashValues(t, rdb, ctx, key, map[string]string{ - hfePersistentField: "11", - hfeLiveField: "21", - hfeExpiredField: "31", - hfeMissingField: "41", - hfeKeeperField: "40", - }) - require.Greater(t, rdb.TTL(ctx, key).Val(), time.Duration(0)) - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 4, - hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField).Val(), []int64{-1, -1, -1, -1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 5) - }) -} - -func TestHashFieldExpirationExpireAndPersistAcrossFieldStates(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - t.Run("hexpire mixed field states", func(t *testing.T) { - key := "hfe-hexpire-mixed" - createHashFieldStates(t, rdb, ctx, key) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 600, "FIELDS", 4, - hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField).Val(), []int64{1, 1, -2, -2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 1) - require.Equal(t, map[string]string{ - hfePersistentField: "10", - hfeLiveField: "20", - hfeKeeperField: "40", - }, rdb.HGetAll(ctx, key).Val()) - }) - - t.Run("hexpire nx only persistent", func(t *testing.T) { - key := "hfe-hexpire-nx" - createHashFieldStates(t, rdb, ctx, key) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 600, "NX", "FIELDS", 4, - hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField).Val(), []int64{1, 0, -2, -2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 1) - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 3, - hfePersistentField, hfeLiveField, hfeKeeperField).Val(), []int64{1, 1, -1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 3) - }) - - t.Run("hexpire xx only live ttl", func(t *testing.T) { - key := "hfe-hexpire-xx" - createHashFieldStates(t, rdb, ctx, key) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 600, "XX", "FIELDS", 4, - hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField).Val(), []int64{0, 1, -2, -2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 2) - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 1, hfeLiveField).Val(), []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 3) - }) - - t.Run("hexpire gt and lt compare against current ttl", func(t *testing.T) { - key := "hfe-hexpire-gt-lt" - createHashFieldStates(t, rdb, ctx, key) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, hfeLiveTTLSeconds-60, "GT", "FIELDS", 1, hfeLiveField).Val(), - []int64{0}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 4, 2) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, hfeLiveTTLSeconds+600, "GT", "FIELDS", 1, hfeLiveField).Val(), - []int64{1}) - afterGT := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, afterGT, 4, 2) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, hfeLiveTTLSeconds+1200, "LT", "FIELDS", 1, hfeLiveField).Val(), - []int64{0}) - require.Equal(t, afterGT, util.GetKMetadata(t, rdb, ctx, key)) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, hfeLiveTTLSeconds, "LT", "FIELDS", 1, hfeLiveField).Val(), - []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 4, 2) - }) - - t.Run("hexpire immediate mixed field states", func(t *testing.T) { - key := "hfe-hexpire-immediate" - createHashFieldStates(t, rdb, ctx, key) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 0, "FIELDS", 4, - hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField).Val(), []int64{2, 2, -2, -2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 1, 1) - require.Equal(t, map[string]string{hfeKeeperField: "40"}, rdb.HGetAll(ctx, key).Val()) - }) - - t.Run("hpersist mixed field states", func(t *testing.T) { - key := "hfe-hpersist-mixed" - createHashFieldStates(t, rdb, ctx, key) - - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 4, - hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField).Val(), []int64{-1, 1, -2, -2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 3) - require.Equal(t, map[string]string{ - hfePersistentField: "10", - hfeLiveField: "20", - hfeKeeperField: "40", - }, rdb.HGetAll(ctx, key).Val()) - }) - }) -} - -func TestHashFieldExpirationExpireCommandFamilyAcrossFieldStates(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - for _, command := range []string{"hexpire", "hpexpire", "hexpireat", "hpexpireat"} { - t.Run(command, func(t *testing.T) { - key := "hfe-expire-family-" + command - createHashFieldStates(t, rdb, ctx, key) - - requireIntArray(t, rdb.Do(ctx, expireCommandArgs(command, key, 10*time.Minute, "FIELDS", 4, - hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField)...).Val(), []int64{1, 1, -2, -2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 1) - require.Equal(t, map[string]string{ - hfePersistentField: "10", - hfeLiveField: "20", - hfeKeeperField: "40", - }, rdb.HGetAll(ctx, key).Val()) - }) - } - }) -} - -func TestHashFieldExpirationExpireCommandFamilyConditions(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - for _, command := range []string{"hexpire", "hpexpire", "hexpireat", "hpexpireat"} { - t.Run(command, func(t *testing.T) { - key := "hfe-expire-family-conditions-" + command - createHashFieldStates(t, rdb, ctx, key) - - requireIntArray(t, rdb.Do(ctx, expireCommandArgs(command, key, 10*time.Minute, "NX", "FIELDS", 4, - hfePersistentField, hfeLiveField, hfeExpiredField, hfeMissingField)...).Val(), []int64{1, 0, -2, -2}) - requireIntArray(t, rdb.Do(ctx, expireCommandArgs(command, key, 12*time.Minute, "XX", "FIELDS", 2, - hfePersistentField, hfeLiveField)...).Val(), []int64{1, 1}) - - requireIntArray(t, rdb.Do(ctx, expireCommandArgs(command, key, time.Minute, "GT", "FIELDS", 2, - hfePersistentField, hfeLiveField)...).Val(), []int64{0, 0}) - requireIntArray(t, rdb.Do(ctx, expireCommandArgs(command, key, 30*time.Minute, "GT", "FIELDS", 1, - hfeLiveField)...).Val(), []int64{1}) - - requireIntArray(t, rdb.Do(ctx, expireCommandArgs(command, key, 40*time.Minute, "LT", "FIELDS", 2, - hfePersistentField, hfeLiveField)...).Val(), []int64{0, 0}) - requireIntArray(t, rdb.Do(ctx, expireCommandArgs(command, key, 20*time.Minute, "LT", "FIELDS", 2, - hfePersistentField, hfeLiveField)...).Val(), []int64{0, 1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 1) - }) - } - }) -} - -func TestHashFieldExpirationExpireCommandFamilyImmediateDelete(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - for _, test := range []struct { - name string - args []interface{} - }{ - {name: "hexpire", args: []interface{}{"hexpire", "KEY", 0, "FIELDS", 3, "a", "b", "missing"}}, - {name: "hpexpire", args: []interface{}{"hpexpire", "KEY", 0, "FIELDS", 3, "a", "b", "missing"}}, - {name: "hexpireat", args: []interface{}{"hexpireat", "KEY", time.Now().Add(-time.Minute).Unix(), "FIELDS", 3, "a", "b", "missing"}}, - {name: "hpexpireat", args: []interface{}{"hpexpireat", "KEY", time.Now().Add(-time.Minute).UnixMilli(), "FIELDS", 3, "a", "b", "missing"}}, - } { - t.Run(test.name, func(t *testing.T) { - key := "hfe-expire-family-immediate-" + test.name - require.Equal(t, int64(3), rdb.HSet(ctx, key, "a", "1", "b", "2", "keeper", "3").Val()) - args := append([]interface{}{}, test.args...) - args[1] = key - - requireIntArray(t, rdb.Do(ctx, args...).Val(), []int64{2, 2, -2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 1, 1) - require.Equal(t, map[string]string{"keeper": "3"}, rdb.HGetAll(ctx, key).Val()) - }) - } - }) -} - -func TestHashFieldExpirationTTLReadCommandsAcrossFieldStates(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-ttl-read-states" - require.Equal(t, int64(3), rdb.HSet(ctx, key, "persist", "1", "live", "2", "expired", "3").Val()) - expireAtMs := time.Now().Add(2*time.Minute + 123*time.Millisecond).UnixMilli() - requireIntArray(t, rdb.Do(ctx, "hpexpireat", key, expireAtMs, "FIELDS", 1, "live").Val(), []int64{1}) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 1, "expired").Val(), []int64{1}) - waitHashFieldExpired(t, rdb, ctx, key, "expired") - before := util.GetKMetadata(t, rdb, ctx, key) - - httl := rdb.Do(ctx, "httl", key, "FIELDS", 4, "persist", "live", "expired", "missing").Val() - httlValues := httl.([]interface{}) - require.Equal(t, int64(-1), httlValues[0]) - require.Greater(t, httlValues[1].(int64), int64(0)) - require.Equal(t, int64(-2), httlValues[2]) - require.Equal(t, int64(-2), httlValues[3]) - - hpttl := rdb.Do(ctx, "hpttl", key, "FIELDS", 4, "persist", "live", "expired", "missing").Val() - hpttlValues := hpttl.([]interface{}) - require.Equal(t, int64(-1), hpttlValues[0]) - require.Greater(t, hpttlValues[1].(int64), int64(0)) - require.LessOrEqual(t, hpttlValues[1].(int64), (2*time.Minute + 123*time.Millisecond).Milliseconds()) - require.Equal(t, int64(-2), hpttlValues[2]) - require.Equal(t, int64(-2), hpttlValues[3]) - - requireIntArray(t, rdb.Do(ctx, "hpexpiretime", key, "FIELDS", 4, "persist", "live", "expired", "missing").Val(), - []int64{-1, expireAtMs, -2, -2}) - expireAtSec := expireAtMs / 1000 - if expireAtMs%1000 != 0 { - expireAtSec++ - } - requireIntArray(t, rdb.Do(ctx, "hexpiretime", key, "FIELDS", 4, "persist", "live", "expired", "missing").Val(), - []int64{-1, expireAtSec, -2, -2}) - require.Equal(t, before, util.GetKMetadata(t, rdb, ctx, key)) - }) -} - -func TestHashFieldExpirationTTLReadCommandRounding(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-ttl-read-rounding" - require.Equal(t, int64(1), rdb.HSet(ctx, key, "field", "1").Val()) - expireAtMs := time.Now().Add(60*time.Second + 123*time.Millisecond).UnixMilli() - if expireAtMs%1000 == 0 { - expireAtMs++ - } - requireIntArray(t, rdb.Do(ctx, "hpexpireat", key, expireAtMs, "FIELDS", 1, "field").Val(), []int64{1}) - - requireIntArray(t, rdb.Do(ctx, "hpexpiretime", key, "FIELDS", 1, "field").Val(), []int64{expireAtMs}) - requireIntArray(t, rdb.Do(ctx, "hexpiretime", key, "FIELDS", 1, "field").Val(), []int64{expireAtMs/1000 + 1}) - - httl := rdb.Do(ctx, "httl", key, "FIELDS", 1, "field").Val().([]interface{})[0].(int64) - require.GreaterOrEqual(t, httl, int64(1)) - require.LessOrEqual(t, httl, int64(61)) - hpttl := rdb.Do(ctx, "hpttl", key, "FIELDS", 1, "field").Val().([]interface{})[0].(int64) - require.Greater(t, hpttl, int64(0)) - require.LessOrEqual(t, hpttl, (60*time.Second + 123*time.Millisecond).Milliseconds()) - }) -} - -func TestHashFieldExpirationCommandFamilyMetadataSequence(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-command-family-metadata" - require.Equal(t, int64(4), rdb.HSet(ctx, key, "p", "1", "a", "2", "b", "3", "c", "4").Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 4, 4) - - requireIntArray(t, rdb.Do(ctx, "hpexpire", key, 60_000, "FIELDS", 1, "a").Val(), []int64{1}) - afterHExpire := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, afterHExpire, 4, 3) - - futureSec, _ := futureUnixTimes(2 * time.Minute) - requireIntArray(t, rdb.Do(ctx, "hexpireat", key, futureSec, "FIELDS", 1, "b").Val(), []int64{1}) - afterHExpireAt := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, afterHExpireAt, 4, 2) - require.LessOrEqual(t, afterHExpireAt.Lower, afterHExpire.Lower) - require.GreaterOrEqual(t, afterHExpireAt.Upper, afterHExpire.Upper) - - pastMs := time.Now().Add(-time.Minute).UnixMilli() - requireIntArray(t, rdb.Do(ctx, "hpexpireat", key, pastMs, "FIELDS", 1, "c").Val(), []int64{2}) - afterImmediate := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, afterImmediate, 3, 1) - - for _, command := range []string{"httl", "hpttl", "hexpiretime", "hpexpiretime"} { - _ = rdb.Do(ctx, command, key, "FIELDS", 4, "p", "a", "b", "c").Val() - require.Equal(t, afterImmediate, util.GetKMetadata(t, rdb, ctx, key)) - } - - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 1, "a").Val(), []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 2) - }) -} - -func TestHashFieldExpirationOptionsAndDuplicates(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-options" - require.Equal(t, int64(3), rdb.HSet(ctx, key, "a", "1", "b", "2", "c", "3").Val()) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 10, "NX", "FIELDS", 2, "a", "a").Val(), []int64{1, 0}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 2) - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 2, "a", "a").Val(), []int64{1, -1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 3) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 0, "GT", "FIELDS", 1, "b").Val(), []int64{0}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 3, 3) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 0, "LT", "FIELDS", 1, "b").Val(), []int64{2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 2) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 0, "FIELDS", 2, "c", "c").Val(), []int64{2, -2}) - require.Equal(t, int64(1), rdb.HLen(ctx, key).Val()) - }) -} - -func TestHashFieldExpirationHLenFastPathAndRepair(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-hlen-repair" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "a", "1", "b", "2").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 1, "a").Val(), []int64{1}) - before := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, before, 2, 1) - - require.Equal(t, int64(2), rdb.HLen(ctx, key).Val()) - require.Equal(t, before, util.GetKMetadata(t, rdb, ctx, key)) - - waitHashFieldExpired(t, rdb, ctx, key, "a") - require.Equal(t, int64(2), rdb.Do(ctx, "hlen", key, "APPROX").Val()) - require.Equal(t, before, util.GetKMetadata(t, rdb, ctx, key)) - require.Equal(t, int64(1), rdb.HLen(ctx, key).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 1, 1) - require.Equal(t, map[string]string{"b": "2"}, rdb.HGetAll(ctx, key).Val()) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 1, "b").Val(), []int64{1}) - waitHashFieldExpired(t, rdb, ctx, key, "b") - require.Equal(t, int64(0), rdb.Do(ctx, "hlen", key, "REPAIR").Val()) - require.Equal(t, int64(0), rdb.Exists(ctx, key).Val()) - require.Error(t, rdb.Do(ctx, "kmetadata", key).Err()) - }) -} - -func TestHashFieldExpirationHLenApproximateConfig(t *testing.T) { - runWithFieldExpirationHashConfigs(t, util.KvrocksServerConfigs{ - "hash-length-mode": "approximate", - }, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-hlen-approx-config" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "a", "1", "b", "2").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 1, "a").Val(), []int64{1}) - before := util.GetKMetadata(t, rdb, ctx, key) - waitHashFieldExpired(t, rdb, ctx, key, "a") - - require.Equal(t, int64(2), rdb.HLen(ctx, key).Val()) - require.Equal(t, before, util.GetKMetadata(t, rdb, ctx, key)) - require.Equal(t, int64(1), rdb.Do(ctx, "hlen", key, "REPAIR").Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 1, 1) - }) -} - -func TestHashFieldExpirationHLenProposalFastPathTimeline(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-hlen-proposal-timeline" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "field1", "value1", "field2", "value2").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 1, "field1").Val(), []int64{1}) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 4, "FIELDS", 1, "field2").Val(), []int64{1}) - - initial := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, initial, 2, 0) - require.Less(t, initial.Lower, initial.Upper) - require.Equal(t, int64(2), rdb.Do(ctx, "hlen", key, "APPROX").Val()) - require.Equal(t, int64(2), rdb.HLen(ctx, key).Val()) - require.Equal(t, initial, util.GetKMetadata(t, rdb, ctx, key)) - - waitHashFieldExpired(t, rdb, ctx, key, "field1") - require.Equal(t, "value2", rdb.HGet(ctx, key, "field2").Val()) - require.Equal(t, int64(2), rdb.Do(ctx, "hlen", key, "APPROX").Val()) - require.Equal(t, initial, util.GetKMetadata(t, rdb, ctx, key)) - - require.Equal(t, int64(1), rdb.HLen(ctx, key).Val()) - afterRepair := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, afterRepair, 1, 0) - require.Equal(t, initial.Upper, afterRepair.Lower) - require.Equal(t, initial.Upper, afterRepair.Upper) - require.Equal(t, int64(1), rdb.Do(ctx, "hlen", key, "APPROX").Val()) - - require.Equal(t, int64(1), rdb.HLen(ctx, key).Val()) - require.Equal(t, afterRepair, util.GetKMetadata(t, rdb, ctx, key)) - - waitHashFieldExpired(t, rdb, ctx, key, "field2") - require.Equal(t, int64(1), rdb.Do(ctx, "hlen", key, "APPROX").Val()) - require.Equal(t, afterRepair, util.GetKMetadata(t, rdb, ctx, key)) - require.Equal(t, int64(0), rdb.HLen(ctx, key).Val()) - require.Equal(t, int64(0), rdb.Exists(ctx, key).Val()) - require.Error(t, rdb.Do(ctx, "kmetadata", key).Err()) - }) -} - -func TestHashFieldExpirationHLenMetadataEffectsByPath(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - t.Run("no ttl candidates fast path does not mutate metadata", func(t *testing.T) { - key := "hfe-hlen-effect-persistent" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "a", "1", "b", "2").Val()) - before := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, before, 2, 2) - - require.Equal(t, int64(2), rdb.Do(ctx, "hlen", key, "APPROX").Val()) - require.Equal(t, int64(2), rdb.HLen(ctx, key).Val()) - require.Equal(t, before, util.GetKMetadata(t, rdb, ctx, key)) - }) - - t.Run("future ttl lower bound fast path does not mutate metadata", func(t *testing.T) { - key := "hfe-hlen-effect-future" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "ttl", "1", "persist", "2").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 300, "FIELDS", 1, "ttl").Val(), []int64{1}) - before := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, before, 2, 1) - - require.Equal(t, int64(2), rdb.Do(ctx, "hlen", key, "APPROX").Val()) - require.Equal(t, int64(2), rdb.HLen(ctx, key).Val()) - require.Equal(t, before, util.GetKMetadata(t, rdb, ctx, key)) - }) - - t.Run("slow repair removes expired ttl candidates and rewrites metadata", func(t *testing.T) { - key := "hfe-hlen-effect-repair" - require.Equal(t, int64(3), rdb.HSet(ctx, key, "persist", "1", "expired", "2", "live", "3").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 1, "expired").Val(), []int64{1}) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 300, "FIELDS", 1, "live").Val(), []int64{1}) - before := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, before, 3, 1) - require.Less(t, before.Lower, before.Upper) - waitHashFieldExpired(t, rdb, ctx, key, "expired") - - require.Equal(t, int64(3), rdb.Do(ctx, "hlen", key, "APPROX").Val()) - require.Equal(t, before, util.GetKMetadata(t, rdb, ctx, key)) - require.Equal(t, int64(2), rdb.HLen(ctx, key).Val()) - afterRepair := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, afterRepair, 2, 1) - require.Equal(t, before.Upper, afterRepair.Lower) - require.Equal(t, before.Upper, afterRepair.Upper) - require.Equal(t, int64(2), rdb.Do(ctx, "hlen", key, "APPROX").Val()) - require.Equal(t, map[string]string{"persist": "1", "live": "3"}, rdb.HGetAll(ctx, key).Val()) - }) - - t.Run("all ttl candidates expired fast delete removes metadata", func(t *testing.T) { - key := "hfe-hlen-effect-fast-delete" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "a", "1", "b", "2").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 2, "a", "b").Val(), []int64{1, 1}) - before := util.GetKMetadata(t, rdb, ctx, key) - requireHashMetadata(t, before, 2, 0) - waitHashFieldExpired(t, rdb, ctx, key, "a") - waitHashFieldExpired(t, rdb, ctx, key, "b") - - require.Equal(t, int64(2), rdb.Do(ctx, "hlen", key, "APPROX").Val()) - require.Equal(t, before, util.GetKMetadata(t, rdb, ctx, key)) - require.Equal(t, int64(0), rdb.HLen(ctx, key).Val()) - require.Equal(t, int64(0), rdb.Exists(ctx, key).Val()) - require.Error(t, rdb.Do(ctx, "kmetadata", key).Err()) - }) - }) -} - -func TestHashFieldExpirationHLenParseErrors(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-hlen-parse" - require.Equal(t, int64(1), rdb.HSet(ctx, key, "a", "1").Val()) - - require.ErrorContains(t, rdb.Do(ctx, "hlen", key, "BAD").Err(), "syntax") - require.ErrorContains(t, rdb.Do(ctx, "hlen", key, "APPROX", "REPAIR").Err(), "wrong number") - }) -} - -func TestHashFieldExpirationHLenReadonlyAndRepairFlags(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - requireHLenCommandInfoFlags(t, rdb, ctx, []interface{}{"readonly"}) - - key := "hfe-hlen-flags" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "a", "1", "b", "2").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 1, "a").Val(), []int64{1}) - waitHashFieldExpired(t, rdb, ctx, key, "a") - - require.ErrorContains(t, - rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1])`, 1, key).Err(), - "Write commands are not allowed from read-only scripts") - require.Equal(t, int64(2), rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1], 'APPROX')`, 1, key).Val()) - require.ErrorContains(t, - rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1], 'REPAIR')`, 1, key).Err(), - "Write commands are not allowed from read-only scripts") - }) -} - -func TestHashFieldExpirationHLenAccurateConfigDynamicFlagsInEvalRO(t *testing.T) { - runWithFieldExpirationHashConfigs(t, util.KvrocksServerConfigs{ - "hash-length-mode": "accurate", - }, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - requireHLenCommandInfoFlags(t, rdb, ctx, []interface{}{"readonly"}) - key := "hfe-hlen-dynamic-accurate" - require.Equal(t, int64(1), rdb.HSet(ctx, key, "a", "1").Val()) - - require.ErrorContains(t, - rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1])`, 1, key).Err(), - "Write commands are not allowed from read-only scripts") - require.Equal(t, int64(1), - rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1], 'APPROX')`, 1, key).Val()) - require.ErrorContains(t, - rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1], 'REPAIR')`, 1, key).Err(), - "Write commands are not allowed from read-only scripts") - }) -} - -func TestHashFieldExpirationHLenApproximateConfigDynamicFlagsInEvalRO(t *testing.T) { - runWithFieldExpirationHashConfigs(t, util.KvrocksServerConfigs{ - "hash-length-mode": "approximate", - }, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - requireHLenCommandInfoFlags(t, rdb, ctx, []interface{}{"readonly"}) - key := "hfe-hlen-dynamic-approx" - require.Equal(t, int64(1), rdb.HSet(ctx, key, "a", "1").Val()) - - require.Equal(t, int64(1), - rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1])`, 1, key).Val()) - require.Equal(t, int64(1), - rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1], 'APPROX')`, 1, key).Val()) - require.ErrorContains(t, - rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1], 'REPAIR')`, 1, key).Err(), - "Write commands are not allowed from read-only scripts") - }) -} - -func TestHashFieldExpirationHLenLegacyAccurateConfigDynamicFlagsInEvalRO(t *testing.T) { - srv := util.StartServer(t, util.KvrocksServerConfigs{ - "hash-encoding-mode": "legacy", - "hash-length-mode": "accurate", - "resp3-enabled": "yes", - }) - defer srv.Close() - - ctx := context.Background() - rdb := srv.NewClient() - defer func() { require.NoError(t, rdb.Close()) }() - - requireHLenCommandInfoFlags(t, rdb, ctx, []interface{}{"readonly"}) - key := "hfe-hlen-dynamic-legacy" - require.Equal(t, int64(1), rdb.HSet(ctx, key, "a", "1").Val()) - require.Equal(t, int64(1), rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1])`, 1, key).Val()) - require.Equal(t, int64(1), rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1], 'APPROX')`, 1, key).Val()) - require.ErrorContains(t, - rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1], 'REPAIR')`, 1, key).Err(), - "Write commands are not allowed from read-only scripts") -} - -func TestHashFieldExpirationHLenLegacyConfigDefaultStaysReadonly(t *testing.T) { - srv := util.StartServer(t, util.KvrocksServerConfigs{ - "hash-encoding-mode": "legacy", - "hash-length-mode": "accurate", - "resp3-enabled": "yes", - }) - defer srv.Close() - - ctx := context.Background() - rdb := srv.NewClient() - defer func() { require.NoError(t, rdb.Close()) }() - - key := "hfe-hlen-legacy-flags" - require.Equal(t, int64(1), rdb.HSet(ctx, key, "a", "1").Val()) - require.Equal(t, int64(1), rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1])`, 1, key).Val()) - require.ErrorContains(t, - rdb.Do(ctx, "eval_ro", `return redis.call('hlen', KEYS[1], 'REPAIR')`, 1, key).Err(), - "Write commands are not allowed from read-only scripts") -} - -func TestHashFieldExpirationLegacyRejectsFieldTTLCommands(t *testing.T) { - srv := util.StartServer(t, util.KvrocksServerConfigs{ - "hash-encoding-mode": "legacy", - "resp3-enabled": "yes", - }) - defer srv.Close() - ctx := context.Background() - rdb := srv.NewClient() - defer func() { require.NoError(t, rdb.Close()) }() - - key := "hfe-legacy" - require.Equal(t, int64(1), rdb.HSet(ctx, key, "a", "1").Val()) - for _, command := range []string{"hexpire", "hpexpire", "hexpireat", "hpexpireat"} { - require.ErrorContains(t, rdb.Do(ctx, command, key, 10, "FIELDS", 1, "a").Err(), "hash field expiration") - } - for _, command := range []string{"hpersist", "httl", "hpttl", "hexpiretime", "hpexpiretime"} { - require.ErrorContains(t, rdb.Do(ctx, command, key, "FIELDS", 1, "a").Err(), "hash field expiration") - } - require.Equal(t, "1", rdb.HGet(ctx, key, "a").Val()) -} - -func TestHashFieldExpirationParseErrors(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-parse" - require.Equal(t, int64(1), rdb.HSet(ctx, key, "a", "1").Val()) - - for _, command := range []string{"hexpire", "hpexpire", "hexpireat", "hpexpireat"} { - t.Run(command, func(t *testing.T) { - for _, test := range []struct { - name string - args []interface{} - errContains string - }{ - {name: "missing fields clause", args: []interface{}{command, key, 10}, errContains: "wrong number"}, - {name: "missing fields clause after option", args: []interface{}{command, key, 10, "NX"}, errContains: "wrong number"}, - {name: "missing numfields", args: []interface{}{command, key, 10, "FIELDS"}, errContains: "wrong number"}, - {name: "numfields is zero", args: []interface{}{command, key, 10, "FIELDS", 0, "a"}, errContains: "integer"}, - {name: "numfields is negative", args: []interface{}{command, key, 10, "FIELDS", -1, "a"}, errContains: "integer"}, - {name: "numfields is not an integer", args: []interface{}{command, key, 10, "FIELDS", "not-int", "a"}, errContains: "integer"}, - {name: "numfields is out of range", args: []interface{}{command, key, 10, "FIELDS", "9223372036854775808", "a"}, errContains: "integer"}, - {name: "has too few fields", args: []interface{}{command, key, 10, "FIELDS", 2, "a"}, errContains: "wrong number"}, - {name: "has extra unknown token after fields", args: []interface{}{command, key, 10, "FIELDS", 1, "a", "BAD"}, errContains: "syntax"}, - {name: "unknown option", args: []interface{}{command, key, 10, "UNKNOWN", "FIELDS", 1, "a"}, errContains: "syntax"}, - {name: "mutually exclusive options", args: []interface{}{command, key, 10, "NX", "XX", "FIELDS", 1, "a"}, errContains: "syntax"}, - {name: "mutually exclusive options after fields", args: []interface{}{command, key, 10, "FIELDS", 1, "a", "NX", "XX"}, errContains: "syntax"}, - {name: "ttl is not an integer", args: []interface{}{command, key, "not-int", "FIELDS", 1, "a"}, errContains: "integer"}, - {name: "ttl is negative", args: []interface{}{command, key, -1, "FIELDS", 1, "a"}, errContains: "invalid expire time"}, - {name: "ttl has trailing characters", args: []interface{}{command, key, "10ms", "FIELDS", 1, "a"}, errContains: "integer"}, - {name: "ttl is out of int64 range", args: []interface{}{command, key, "9223372036854775808", "FIELDS", 1, "a"}, errContains: "integer"}, - } { - t.Run(test.name, func(t *testing.T) { - require.ErrorContains(t, rdb.Do(ctx, test.args...).Err(), test.errContains) - }) - } - }) - } - - for _, command := range []string{"hpersist", "httl", "hpttl", "hexpiretime", "hpexpiretime"} { - t.Run(command, func(t *testing.T) { - for _, test := range []struct { - name string - args []interface{} - errContains string - }{ - {name: "missing fields clause", args: []interface{}{command, key}, errContains: "wrong number"}, - {name: "wrong fields keyword", args: []interface{}{command, key, "FIELD", 1, "a"}, errContains: "syntax"}, - {name: "missing numfields", args: []interface{}{command, key, "FIELDS"}, errContains: "wrong number"}, - {name: "numfields is zero", args: []interface{}{command, key, "FIELDS", 0, "a"}, errContains: "integer"}, - {name: "numfields is negative", args: []interface{}{command, key, "FIELDS", -1, "a"}, errContains: "integer"}, - {name: "numfields is not an integer", args: []interface{}{command, key, "FIELDS", "not-int", "a"}, errContains: "integer"}, - {name: "numfields is out of range", args: []interface{}{command, key, "FIELDS", "9223372036854775808", "a"}, errContains: "integer"}, - {name: "has too few fields", args: []interface{}{command, key, "FIELDS", 2, "a"}, errContains: "wrong number"}, - {name: "has too many fields", args: []interface{}{command, key, "FIELDS", 1, "a", "b"}, errContains: "wrong number"}, - } { - t.Run(test.name, func(t *testing.T) { - require.ErrorContains(t, rdb.Do(ctx, test.args...).Err(), test.errContains) - }) - } - }) - } - }) -} - -func TestHashFieldExpirationExpireCommandFamilyTimeBoundaries(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - nowMs := time.Now().UnixMilli() - nowSec := nowMs / 1000 - - for _, test := range []struct { - name string - command string - valid int64 - invalid int64 - }{ - { - name: "hexpire", - command: "hexpire", - valid: (hfeMaxAbsTimeMs - nowMs - 10_000) / 1000, - invalid: (hfeMaxAbsTimeMs - nowMs + 10_000) / 1000, - }, - { - name: "hpexpire", - command: "hpexpire", - valid: hfeMaxAbsTimeMs - nowMs - 10_000, - invalid: hfeMaxAbsTimeMs - nowMs + 10_000, - }, - { - name: "hexpireat", - command: "hexpireat", - valid: hfeMaxAbsTimeMs/1000 - 1, - invalid: hfeMaxAbsTimeMs/1000 + nowSec, - }, - { - name: "hpexpireat", - command: "hpexpireat", - valid: hfeMaxAbsTimeMs - 1, - invalid: hfeMaxAbsTimeMs + nowMs, - }, - } { - t.Run(test.name, func(t *testing.T) { - require.Positive(t, test.valid) - require.Greater(t, test.invalid, test.valid) - - key := "hfe-time-boundary-" + test.name - require.Equal(t, int64(1), rdb.HSet(ctx, key, "field", "value").Val()) - before := util.GetKMetadata(t, rdb, ctx, key) - - requireIntArray(t, rdb.Do(ctx, test.command, key, test.valid, "FIELDS", 1, "field").Val(), []int64{1}) - require.ErrorContains(t, rdb.Do(ctx, test.command, key, test.invalid, "FIELDS", 1, "field").Err(), "expire time") - require.Equal(t, "value", rdb.HGet(ctx, key, "field").Val()) - require.NotEqual(t, before, util.GetKMetadata(t, rdb, ctx, key)) - }) - } - }) -} - -func TestHashFieldExpirationExpireCommandParserRedisCompatibleSuccess(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - for _, command := range []string{"hexpire", "hpexpire", "hexpireat", "hpexpireat"} { - t.Run(command, func(t *testing.T) { - key := "hfe-expire-parser-" + command - require.Equal(t, int64(2), rdb.HSet(ctx, key, "a", "1", "b", "2").Val()) - - requireIntArray(t, rdb.Do(ctx, expireCommandArgs(command, key, time.Minute, "FIELDS", 1, "a", "NX")...).Val(), - []int64{1}) - requireIntArray(t, rdb.Do(ctx, expireCommandArgs(command, key, time.Minute, "NX", "NX", "FIELDS", 1, "b")...).Val(), - []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 0) - }) - } - }) -} - -func TestHashFieldExpirationKeywordLikeFieldNames(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-keyword-like-fields" - require.Equal(t, int64(5), rdb.HSet(ctx, key, - "EX", "1", - "PX", "2", - "FIELDS", "3", - "NX", "4", - "60", "5").Val()) - - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 120, "FIELDS", 5, "EX", "PX", "FIELDS", "NX", "60").Val(), - []int64{1, 1, 1, 1, 1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 5, 0) - - ttl := rdb.Do(ctx, "httl", key, "FIELDS", 5, "EX", "PX", "FIELDS", "NX", "60").Val().([]interface{}) - for _, result := range ttl { - require.Greater(t, result.(int64), int64(0)) - require.LessOrEqual(t, result.(int64), int64(120)) - } - }) -} - -func TestHashFieldExpirationCommandInfoForCommandFamily(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - for _, command := range []string{"hexpire", "hpexpire", "hexpireat", "hpexpireat"} { - requireCommandInfo(t, rdb, ctx, command, -6, false) - } - for _, command := range []string{"hpersist"} { - requireCommandInfo(t, rdb, ctx, command, -5, false) - } - for _, command := range []string{"httl", "hpttl", "hexpiretime", "hpexpiretime"} { - requireCommandInfo(t, rdb, ctx, command, -5, true) - } - }) -} - -func TestHashFieldExpirationInputCornerCases(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - t.Run("hexpire zero ttl deletes immediately", func(t *testing.T) { - key := "hfe-zero-ttl" - require.Equal(t, int64(3), rdb.HSet(ctx, key, "a", "1", "b", "2", "keeper", "3").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 0, "FIELDS", 3, "a", "b", "missing").Val(), - []int64{2, 2, -2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 1, 1) - require.Equal(t, map[string]string{"keeper": "3"}, rdb.HGetAll(ctx, key).Val()) - require.ErrorIs(t, rdb.HGet(ctx, key, "a").Err(), redis.Nil) - require.ErrorIs(t, rdb.HGet(ctx, key, "b").Err(), redis.Nil) - }) - - t.Run("hexpire and hpersist return missing for missing key", func(t *testing.T) { - key := "hfe-missing-key" - require.Equal(t, int64(0), rdb.Exists(ctx, key).Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 0, "FIELDS", 2, "a", "b").Val(), []int64{-2, -2}) - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 2, "a", "b").Val(), []int64{-2, -2}) - for _, command := range []string{"hpexpire", "hexpireat", "hpexpireat"} { - requireIntArray(t, rdb.Do(ctx, expireCommandArgs(command, key, time.Minute, "FIELDS", 2, "a", "b")...).Val(), - []int64{-2, -2}) - } - for _, command := range []string{"httl", "hpttl", "hexpiretime", "hpexpiretime"} { - requireIntArray(t, rdb.Do(ctx, command, key, "FIELDS", 2, "a", "b").Val(), []int64{-2, -2}) - } - require.Equal(t, int64(0), rdb.Exists(ctx, key).Val()) - }) - - t.Run("hexpire ttl overflow leaves field and metadata unchanged", func(t *testing.T) { - key := "hfe-ttl-overflow" - require.Equal(t, int64(1), rdb.HSet(ctx, key, "a", "1").Val()) - before := util.GetKMetadata(t, rdb, ctx, key) - require.ErrorContains(t, rdb.Do(ctx, "hexpire", key, "9223372036854775807", "FIELDS", 1, "a").Err(), - "overflow") - require.Equal(t, before, util.GetKMetadata(t, rdb, ctx, key)) - require.Equal(t, "1", rdb.HGet(ctx, key, "a").Val()) - }) - - t.Run("hexpire and hpersist reject wrong type", func(t *testing.T) { - key := "hfe-wrong-type" - require.NoError(t, rdb.Set(ctx, key, "value", 0).Err()) - require.ErrorContains(t, rdb.Do(ctx, "hexpire", key, 10, "FIELDS", 1, "a").Err(), "WRONGTYPE") - require.ErrorContains(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 1, "a").Err(), "WRONGTYPE") - require.ErrorContains(t, rdb.Do(ctx, "hpexpire", key, 10, "FIELDS", 1, "a").Err(), "WRONGTYPE") - require.ErrorContains(t, rdb.Do(ctx, "hexpireat", key, 10, "FIELDS", 1, "a").Err(), "WRONGTYPE") - require.ErrorContains(t, rdb.Do(ctx, "hpexpireat", key, 10, "FIELDS", 1, "a").Err(), "WRONGTYPE") - require.ErrorContains(t, rdb.Do(ctx, "httl", key, "FIELDS", 1, "a").Err(), "WRONGTYPE") - require.ErrorContains(t, rdb.Do(ctx, "hpttl", key, "FIELDS", 1, "a").Err(), "WRONGTYPE") - require.ErrorContains(t, rdb.Do(ctx, "hexpiretime", key, "FIELDS", 1, "a").Err(), "WRONGTYPE") - require.ErrorContains(t, rdb.Do(ctx, "hpexpiretime", key, "FIELDS", 1, "a").Err(), "WRONGTYPE") - require.Equal(t, "value", rdb.Get(ctx, key).Val()) - }) - - t.Run("keywords and command name are case insensitive", func(t *testing.T) { - key := "hfe-case-insensitive" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "a", "1", "b", "2").Val()) - requireIntArray(t, rdb.Do(ctx, "hExPiRe", key, 60, "nX", "fIeLdS", 1, "a").Val(), []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 1) - requireIntArray(t, rdb.Do(ctx, "hPeRsIsT", key, "fIeLdS", 1, "a").Val(), []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 2) - }) - - t.Run("empty field name is valid", func(t *testing.T) { - key := "hfe-empty-field" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "", "empty", "normal", "value").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 60, "FIELDS", 1, "").Val(), []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 1) - requireIntArray(t, rdb.Do(ctx, "hpersist", key, "FIELDS", 1, "").Val(), []int64{1}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 2) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 0, "FIELDS", 1, "").Val(), []int64{2}) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 1, 1) - require.Equal(t, map[string]string{"normal": "value"}, rdb.HGetAll(ctx, key).Val()) - }) - }) -} - -func TestHashFieldExpirationReadCommandSet(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-read-command-set" - require.Equal(t, int64(4), rdb.HSet(ctx, key, "a", "1", "b", "2", "c", "3", "d", "4").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 2, "a", "c").Val(), []int64{1, 1}) - waitHashFieldExpired(t, rdb, ctx, key, "a") - waitHashFieldExpired(t, rdb, ctx, key, "c") - - keys := rdb.HKeys(ctx, key).Val() - sort.Strings(keys) - require.Equal(t, []string{"b", "d"}, keys) - require.ElementsMatch(t, []string{"2", "4"}, rdb.HVals(ctx, key).Val()) - require.Equal(t, []interface{}{"b", "2", "d", "4"}, rdb.Do(ctx, "hrangebylex", key, "[a", "[z").Val()) - require.Equal(t, []interface{}{"d", "4"}, rdb.Do(ctx, "hrangebylex", key, "[a", "[z", "LIMIT", 1, 1).Val()) - }) -} - -func TestHashFieldExpirationRandFieldAllExpired(t *testing.T) { - runWithFieldExpirationHash(t, func(t *testing.T, rdb *redis.Client, ctx context.Context) { - key := "hfe-rand-all-expired" - require.Equal(t, int64(2), rdb.HSet(ctx, key, "a", "1", "b", "2").Val()) - requireIntArray(t, rdb.Do(ctx, "hexpire", key, 1, "FIELDS", 2, "a", "b").Val(), []int64{1, 1}) - waitHashFieldExpired(t, rdb, ctx, key, "a") - waitHashFieldExpired(t, rdb, ctx, key, "b") - - require.Nil(t, rdb.Do(ctx, "hrandfield", key).Val()) - require.Equal(t, []interface{}{}, rdb.Do(ctx, "hrandfield", key, 10).Val()) - requireHashMetadata(t, util.GetKMetadata(t, rdb, ctx, key), 2, 0) - }) -}
