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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/storage/redis_db.cc
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,14 @@ rocksdb::Status Database::TTL(engine::Context &ctx, const Slice &user_key, int64
Metadata metadata(kRedisNone, false);
s = metadata.Decode(value);
if (!s.ok()) return s;
// Report -2 when the metadata is expired, so that TTL stays consistent with
// EXISTS and TYPE which also rely on Metadata::Expired(). This also covers
// containers whose metadata is kept but treated as expired once empty (e.g. a
// hash whose last field was deleted, leaving size == 0 but expire == 0).
if (metadata.Expired()) {
*ttl = -2;
return rocksdb::Status::OK();
}
*ttl = metadata.TTL();

return rocksdb::Status::OK();
Expand Down
28 changes: 28 additions & 0 deletions tests/cppunit/types/hash_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,34 @@ TEST_F(RedisHashTest, GetAndSet) {
s = hash_->Del(*ctx_, key_);
}

TEST_F(RedisHashTest, TTLReportsKeyGoneAfterDeletingLastField) {
uint64_t ret = 0;
auto s = hash_->Set(*ctx_, key_, fields_[0], values_[0], &ret);
EXPECT_TRUE(s.ok() && ret == 1);

redis::Database db(storage_.get(), "hash_ns");
int64_t ttl = 0;
// The hash holds a live field but has no key-level expire, so TTL is -1.
s = db.TTL(*ctx_, key_, &ttl);
EXPECT_TRUE(s.ok());
EXPECT_EQ(-1, ttl);

// Deleting the last field leaves an empty hash whose metadata is still stored.
// TTL must report -2 (key does not exist) to stay consistent with EXISTS,
// instead of -1 leaked from the leftover metadata.
s = hash_->Delete(*ctx_, key_, {fields_[0]}, &ret);
EXPECT_TRUE(s.ok() && ret == 1);

uint32_t exists = 0;
s = db.Exists(*ctx_, {Slice(key_)}, &exists);
EXPECT_TRUE(s.ok());
EXPECT_EQ(0u, exists);

s = db.TTL(*ctx_, key_, &ttl);
EXPECT_TRUE(s.ok());
EXPECT_EQ(-2, ttl);
}

TEST_F(RedisHashTest, MGetAndMSet) {
uint64_t ret = 0;
std::vector<FieldValue> fvs;
Expand Down
4 changes: 4 additions & 0 deletions tests/gocase/unit/type/hash/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,10 @@ var testHash = func(t *testing.T, configs util.KvrocksServerConfigs) {
rdb.HMSet(ctx, "myhash", []string{"a", "1", "b", "2", "c", "3"})
require.Equal(t, int64(3), rdb.HDel(ctx, "myhash", "a", "b", "c", "d", "e").Val())
require.Equal(t, int64(0), rdb.Exists(ctx, "myhash").Val())
// TTL and PTTL must report -2 (key does not exist) once the last field is
// deleted, consistent with EXISTS, instead of -1 from the leftover metadata.
require.EqualValues(t, -2, rdb.TTL(ctx, "myhash").Val())
require.EqualValues(t, -2, rdb.PTTL(ctx, "myhash").Val())
})

t.Run("HEXISTS", func(t *testing.T) {
Expand Down
Loading