This is an automated email from the ASF dual-hosted git repository.

hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git


The following commit(s) were added to refs/heads/unstable by this push:
     new 7a08dfdc Add support of EXPIRETIME and PEXPIRETIME (#1965)
7a08dfdc is described below

commit 7a08dfdccea05266992d95e1b22c616e17bdbb35
Author: kay011 <[email protected]>
AuthorDate: Tue Dec 26 21:07:05 2023 +0800

    Add support of EXPIRETIME and PEXPIRETIME (#1965)
---
 src/commands/cmd_key.cc                 | 36 +++++++++++++++
 src/stats/disk_stats.cc                 | 14 +++---
 src/storage/redis_db.cc                 | 22 ++++++---
 src/storage/redis_db.h                  |  5 ++-
 src/types/redis_bloom_chain.cc          |  2 +-
 src/types/redis_hash.cc                 |  2 +-
 src/types/redis_json.cc                 |  4 +-
 src/types/redis_list.cc                 |  2 +-
 src/types/redis_set.cc                  |  2 +-
 src/types/redis_sortedint.cc            |  2 +-
 src/types/redis_stream.cc               |  2 +-
 src/types/redis_zset.cc                 |  2 +-
 tests/cppunit/metadata_test.cc          | 79 ++++++++++++++++++++++++++++++++-
 tests/gocase/unit/expire/expire_test.go | 52 ++++++++++++++++++++++
 14 files changed, 201 insertions(+), 25 deletions(-)

diff --git a/src/commands/cmd_key.cc b/src/commands/cmd_key.cc
index 5cea1d70..35bfeefd 100644
--- a/src/commands/cmd_key.cc
+++ b/src/commands/cmd_key.cc
@@ -233,6 +233,40 @@ class CommandPExpireAt : public Commander {
   uint64_t timestamp_ = 0;
 };
 
+class CommandExpireTime : public Commander {
+ public:
+  Status Execute(Server *srv, Connection *conn, std::string *output) override {
+    redis::Database redis(srv->storage, conn->GetNamespace());
+    uint64_t timestamp = 0;
+    auto s = redis.GetExpireTime(args_[1], &timestamp);
+    if (s.ok()) {
+      *output = timestamp > 0 ? redis::Integer(timestamp / 1000) : 
redis::Integer(-1);
+    } else if (s.IsNotFound()) {
+      *output = redis::Integer(-2);
+    } else {
+      return {Status::RedisExecErr, s.ToString()};
+    }
+    return Status::OK();
+  }
+};
+
+class CommandPExpireTime : public Commander {
+ public:
+  Status Execute(Server *srv, Connection *conn, std::string *output) override {
+    redis::Database redis(srv->storage, conn->GetNamespace());
+    uint64_t timestamp = 0;
+    auto s = redis.GetExpireTime(args_[1], &timestamp);
+    if (s.ok()) {
+      *output = timestamp > 0 ? redis::Integer(timestamp) : redis::Integer(-1);
+    } else if (s.IsNotFound()) {
+      *output = redis::Integer(-2);
+    } else {
+      return {Status::RedisExecErr, s.ToString()};
+    }
+    return Status::OK();
+  }
+};
+
 class CommandPersist : public Commander {
  public:
   Status Execute(Server *srv, Connection *conn, std::string *output) override {
@@ -284,6 +318,8 @@ REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandTTL>("ttl", 2, 
"read-only", 1, 1, 1),
                         MakeCmdAttr<CommandPExpire>("pexpire", 3, "write", 1, 
1, 1),
                         MakeCmdAttr<CommandExpireAt>("expireat", 3, "write", 
1, 1, 1),
                         MakeCmdAttr<CommandPExpireAt>("pexpireat", 3, "write", 
1, 1, 1),
+                        MakeCmdAttr<CommandExpireTime>("expiretime", 2, 
"read-only", 1, 1, 1),
+                        MakeCmdAttr<CommandPExpireTime>("pexpiretime", 2, 
"read-only", 1, 1, 1),
                         MakeCmdAttr<CommandDel>("del", -2, "write", 1, -1, 1),
                         MakeCmdAttr<CommandDel>("unlink", -2, "write", 1, -1, 
1), )
 
diff --git a/src/stats/disk_stats.cc b/src/stats/disk_stats.cc
index a5e946eb..7c3c9998 100644
--- a/src/stats/disk_stats.cc
+++ b/src/stats/disk_stats.cc
@@ -77,21 +77,21 @@ rocksdb::Status Disk::GetStringSize(const Slice &ns_key, 
uint64_t *key_size) {
 
 rocksdb::Status Disk::GetHashSize(const Slice &ns_key, uint64_t *key_size) {
   HashMetadata metadata(false);
-  rocksdb::Status s = Database::GetMetadata(kRedisHash, ns_key, &metadata);
+  rocksdb::Status s = Database::GetMetadata({kRedisHash}, ns_key, &metadata);
   if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
   return GetApproximateSizes(metadata, ns_key, 
storage_->GetCFHandle(engine::kSubkeyColumnFamilyName), key_size);
 }
 
 rocksdb::Status Disk::GetSetSize(const Slice &ns_key, uint64_t *key_size) {
   SetMetadata metadata(false);
-  rocksdb::Status s = Database::GetMetadata(kRedisSet, ns_key, &metadata);
+  rocksdb::Status s = Database::GetMetadata({kRedisSet}, ns_key, &metadata);
   if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
   return GetApproximateSizes(metadata, ns_key, 
storage_->GetCFHandle(engine::kSubkeyColumnFamilyName), key_size);
 }
 
 rocksdb::Status Disk::GetListSize(const Slice &ns_key, uint64_t *key_size) {
   ListMetadata metadata(false);
-  rocksdb::Status s = Database::GetMetadata(kRedisList, ns_key, &metadata);
+  rocksdb::Status s = Database::GetMetadata({kRedisList}, ns_key, &metadata);
   if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
   std::string buf;
   PutFixed64(&buf, metadata.head);
@@ -100,7 +100,7 @@ rocksdb::Status Disk::GetListSize(const Slice &ns_key, 
uint64_t *key_size) {
 
 rocksdb::Status Disk::GetZsetSize(const Slice &ns_key, uint64_t *key_size) {
   ZSetMetadata metadata(false);
-  rocksdb::Status s = Database::GetMetadata(kRedisZSet, ns_key, &metadata);
+  rocksdb::Status s = Database::GetMetadata({kRedisZSet}, ns_key, &metadata);
   if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
   std::string score_bytes;
   PutDouble(&score_bytes, kMinScore);
@@ -112,7 +112,7 @@ rocksdb::Status Disk::GetZsetSize(const Slice &ns_key, 
uint64_t *key_size) {
 
 rocksdb::Status Disk::GetBitmapSize(const Slice &ns_key, uint64_t *key_size) {
   BitmapMetadata metadata(false);
-  rocksdb::Status s = Database::GetMetadata(kRedisBitmap, ns_key, &metadata);
+  rocksdb::Status s = Database::GetMetadata({kRedisBitmap}, ns_key, &metadata);
   if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
   return GetApproximateSizes(metadata, ns_key, 
storage_->GetCFHandle(engine::kSubkeyColumnFamilyName), key_size,
                              std::to_string(0), std::to_string(0));
@@ -120,7 +120,7 @@ rocksdb::Status Disk::GetBitmapSize(const Slice &ns_key, 
uint64_t *key_size) {
 
 rocksdb::Status Disk::GetSortedintSize(const Slice &ns_key, uint64_t 
*key_size) {
   SortedintMetadata metadata(false);
-  rocksdb::Status s = Database::GetMetadata(kRedisSortedint, ns_key, 
&metadata);
+  rocksdb::Status s = Database::GetMetadata({kRedisSortedint}, ns_key, 
&metadata);
   if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
   std::string start_buf;
   PutFixed64(&start_buf, 0);
@@ -130,7 +130,7 @@ rocksdb::Status Disk::GetSortedintSize(const Slice &ns_key, 
uint64_t *key_size)
 
 rocksdb::Status Disk::GetStreamSize(const Slice &ns_key, uint64_t *key_size) {
   StreamMetadata metadata(false);
-  rocksdb::Status s = Database::GetMetadata(kRedisStream, ns_key, &metadata);
+  rocksdb::Status s = Database::GetMetadata({kRedisStream}, ns_key, &metadata);
   if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
   return GetApproximateSizes(metadata, ns_key, 
storage_->GetCFHandle(engine::kStreamColumnFamilyName), key_size);
 }
diff --git a/src/storage/redis_db.cc b/src/storage/redis_db.cc
index 7708b491..a3734730 100644
--- a/src/storage/redis_db.cc
+++ b/src/storage/redis_db.cc
@@ -67,18 +67,18 @@ rocksdb::Status Database::ParseMetadata(RedisTypes types, 
Slice *bytes, Metadata
   return s;
 }
 
-rocksdb::Status Database::GetMetadata(RedisType type, const Slice &ns_key, 
Metadata *metadata) {
+rocksdb::Status Database::GetMetadata(RedisTypes types, const Slice &ns_key, 
Metadata *metadata) {
   std::string raw_value;
   Slice rest;
-  return GetMetadata(type, ns_key, &raw_value, metadata, &rest);
+  return GetMetadata(types, ns_key, &raw_value, metadata, &rest);
 }
 
-rocksdb::Status Database::GetMetadata(RedisType type, const Slice &ns_key, 
std::string *raw_value, Metadata *metadata,
+rocksdb::Status Database::GetMetadata(RedisTypes types, const Slice &ns_key, 
std::string *raw_value, Metadata *metadata,
                                       Slice *rest) {
   auto s = GetRawMetadata(ns_key, raw_value);
   *rest = *raw_value;
   if (!s.ok()) return s;
-  return ParseMetadata({type}, rest, metadata);
+  return ParseMetadata(types, rest, metadata);
 }
 
 rocksdb::Status Database::GetRawMetadata(const Slice &ns_key, std::string 
*bytes) {
@@ -225,6 +225,16 @@ rocksdb::Status Database::TTL(const Slice &user_key, 
int64_t *ttl) {
   return rocksdb::Status::OK();
 }
 
+rocksdb::Status Database::GetExpireTime(const Slice &user_key, uint64_t 
*timestamp) {
+  std::string ns_key = AppendNamespacePrefix(user_key);
+  Metadata metadata(kRedisNone, false);
+  auto s = GetMetadata(RedisTypes::All(), ns_key, &metadata);
+  if (!s.ok()) return s;
+  *timestamp = metadata.expire;
+
+  return rocksdb::Status::OK();
+}
+
 rocksdb::Status Database::GetKeyNumStats(const std::string &prefix, 
KeyNumStats *stats) {
   return Keys(prefix, nullptr, stats);
 }
@@ -488,7 +498,7 @@ rocksdb::Status Database::Dump(const Slice &user_key, 
std::vector<std::string> *
 
   if (metadata.Type() == kRedisList) {
     ListMetadata list_metadata(false);
-    s = GetMetadata(kRedisList, ns_key, &list_metadata);
+    s = GetMetadata({kRedisList}, ns_key, &list_metadata);
     if (!s.ok()) return s.IsNotFound() ? rocksdb::Status::OK() : s;
     infos->emplace_back("head");
     infos->emplace_back(std::to_string(list_metadata.head));
@@ -644,7 +654,7 @@ rocksdb::Status SubKeyScanner::Scan(RedisType type, const 
Slice &user_key, const
   uint64_t cnt = 0;
   std::string ns_key = AppendNamespacePrefix(user_key);
   Metadata metadata(type, false);
-  rocksdb::Status s = GetMetadata(type, ns_key, &metadata);
+  rocksdb::Status s = GetMetadata({type}, ns_key, &metadata);
   if (!s.ok()) return s;
 
   LatestSnapShot ss(storage_);
diff --git a/src/storage/redis_db.h b/src/storage/redis_db.h
index 181723a4..7258923c 100644
--- a/src/storage/redis_db.h
+++ b/src/storage/redis_db.h
@@ -35,8 +35,8 @@ class Database {
 
   explicit Database(engine::Storage *storage, std::string ns = "");
   [[nodiscard]] static rocksdb::Status ParseMetadata(RedisTypes types, Slice 
*bytes, Metadata *metadata);
-  [[nodiscard]] rocksdb::Status GetMetadata(RedisType type, const Slice 
&ns_key, Metadata *metadata);
-  [[nodiscard]] rocksdb::Status GetMetadata(RedisType type, const Slice 
&ns_key, std::string *raw_value,
+  [[nodiscard]] rocksdb::Status GetMetadata(RedisTypes types, const Slice 
&ns_key, Metadata *metadata);
+  [[nodiscard]] rocksdb::Status GetMetadata(RedisTypes types, const Slice 
&ns_key, std::string *raw_value,
                                             Metadata *metadata, Slice *rest);
   [[nodiscard]] rocksdb::Status GetRawMetadata(const Slice &ns_key, 
std::string *bytes);
   [[nodiscard]] rocksdb::Status Expire(const Slice &user_key, uint64_t 
timestamp);
@@ -44,6 +44,7 @@ class Database {
   [[nodiscard]] rocksdb::Status MDel(const std::vector<Slice> &keys, uint64_t 
*deleted_cnt);
   [[nodiscard]] rocksdb::Status Exists(const std::vector<Slice> &keys, int 
*ret);
   [[nodiscard]] rocksdb::Status TTL(const Slice &user_key, int64_t *ttl);
+  [[nodiscard]] rocksdb::Status GetExpireTime(const Slice &user_key, uint64_t 
*timestamp);
   [[nodiscard]] rocksdb::Status Type(const Slice &user_key, RedisType *type);
   [[nodiscard]] rocksdb::Status Dump(const Slice &user_key, 
std::vector<std::string> *infos);
   [[nodiscard]] rocksdb::Status FlushDB();
diff --git a/src/types/redis_bloom_chain.cc b/src/types/redis_bloom_chain.cc
index 4c4e32d0..90a43f05 100644
--- a/src/types/redis_bloom_chain.cc
+++ b/src/types/redis_bloom_chain.cc
@@ -25,7 +25,7 @@
 namespace redis {
 
 rocksdb::Status BloomChain::getBloomChainMetadata(const Slice &ns_key, 
BloomChainMetadata *metadata) {
-  return Database::GetMetadata(kRedisBloomFilter, ns_key, metadata);
+  return Database::GetMetadata({kRedisBloomFilter}, ns_key, metadata);
 }
 
 std::string BloomChain::getBFKey(const Slice &ns_key, const BloomChainMetadata 
&metadata, uint16_t filters_index) {
diff --git a/src/types/redis_hash.cc b/src/types/redis_hash.cc
index c2e348cf..0ee9c16e 100644
--- a/src/types/redis_hash.cc
+++ b/src/types/redis_hash.cc
@@ -34,7 +34,7 @@
 namespace redis {
 
 rocksdb::Status Hash::GetMetadata(const Slice &ns_key, HashMetadata *metadata) 
{
-  return Database::GetMetadata(kRedisHash, ns_key, metadata);
+  return Database::GetMetadata({kRedisHash}, ns_key, metadata);
 }
 
 rocksdb::Status Hash::Size(const Slice &user_key, uint64_t *size) {
diff --git a/src/types/redis_json.cc b/src/types/redis_json.cc
index b7c0d4c4..fb3add5f 100644
--- a/src/types/redis_json.cc
+++ b/src/types/redis_json.cc
@@ -74,7 +74,7 @@ rocksdb::Status Json::read(const Slice &ns_key, JsonMetadata 
*metadata, JsonValu
   std::string bytes;
   Slice rest;
 
-  auto s = GetMetadata(kRedisJson, ns_key, &bytes, metadata, &rest);
+  auto s = GetMetadata({kRedisJson}, ns_key, &bytes, metadata, &rest);
   if (!s.ok()) return s;
 
   return parse(*metadata, rest, value);
@@ -105,7 +105,7 @@ rocksdb::Status Json::Info(const std::string &user_key, 
JsonStorageFormat *stora
   Slice rest;
   JsonMetadata metadata;
 
-  auto s = GetMetadata(kRedisJson, ns_key, &bytes, &metadata, &rest);
+  auto s = GetMetadata({kRedisJson}, ns_key, &bytes, &metadata, &rest);
   if (!s.ok()) return s;
 
   *storage_format = metadata.format;
diff --git a/src/types/redis_list.cc b/src/types/redis_list.cc
index b862a330..ed67007e 100644
--- a/src/types/redis_list.cc
+++ b/src/types/redis_list.cc
@@ -28,7 +28,7 @@
 namespace redis {
 
 rocksdb::Status List::GetMetadata(const Slice &ns_key, ListMetadata *metadata) 
{
-  return Database::GetMetadata(kRedisList, ns_key, metadata);
+  return Database::GetMetadata({kRedisList}, ns_key, metadata);
 }
 
 rocksdb::Status List::Size(const Slice &user_key, uint64_t *size) {
diff --git a/src/types/redis_set.cc b/src/types/redis_set.cc
index ddfd6c0b..32c06ef9 100644
--- a/src/types/redis_set.cc
+++ b/src/types/redis_set.cc
@@ -29,7 +29,7 @@
 namespace redis {
 
 rocksdb::Status Set::GetMetadata(const Slice &ns_key, SetMetadata *metadata) {
-  return Database::GetMetadata(kRedisSet, ns_key, metadata);
+  return Database::GetMetadata({kRedisSet}, ns_key, metadata);
 }
 
 // Make sure members are uniq before use Overwrite
diff --git a/src/types/redis_sortedint.cc b/src/types/redis_sortedint.cc
index 76ad31fc..6670eb74 100644
--- a/src/types/redis_sortedint.cc
+++ b/src/types/redis_sortedint.cc
@@ -29,7 +29,7 @@
 namespace redis {
 
 rocksdb::Status Sortedint::GetMetadata(const Slice &ns_key, SortedintMetadata 
*metadata) {
-  return Database::GetMetadata(kRedisSortedint, ns_key, metadata);
+  return Database::GetMetadata({kRedisSortedint}, ns_key, metadata);
 }
 
 rocksdb::Status Sortedint::Add(const Slice &user_key, const 
std::vector<uint64_t> &ids, uint64_t *added_cnt) {
diff --git a/src/types/redis_stream.cc b/src/types/redis_stream.cc
index bc050ad0..48fe7928 100644
--- a/src/types/redis_stream.cc
+++ b/src/types/redis_stream.cc
@@ -45,7 +45,7 @@ const char *errXGroupSubcommandRequiresKeyExist =
 Note that for CREATE you may want to use the MKSTREAM option to create an 
empty stream automatically.";
 
 rocksdb::Status Stream::GetMetadata(const Slice &stream_name, StreamMetadata 
*metadata) {
-  return Database::GetMetadata(kRedisStream, stream_name, metadata);
+  return Database::GetMetadata({kRedisStream}, stream_name, metadata);
 }
 
 rocksdb::Status Stream::GetLastGeneratedID(const Slice &stream_name, 
StreamEntryID *id) {
diff --git a/src/types/redis_zset.cc b/src/types/redis_zset.cc
index c99d3c8a..92bf5be7 100644
--- a/src/types/redis_zset.cc
+++ b/src/types/redis_zset.cc
@@ -32,7 +32,7 @@
 namespace redis {
 
 rocksdb::Status ZSet::GetMetadata(const Slice &ns_key, ZSetMetadata *metadata) 
{
-  return Database::GetMetadata(kRedisZSet, ns_key, metadata);
+  return Database::GetMetadata({kRedisZSet}, ns_key, metadata);
 }
 
 rocksdb::Status ZSet::Add(const Slice &user_key, ZAddFlags flags, MemberScores 
*mscores, uint64_t *added_cnt) {
diff --git a/tests/cppunit/metadata_test.cc b/tests/cppunit/metadata_test.cc
index a454973b..e17735c3 100644
--- a/tests/cppunit/metadata_test.cc
+++ b/tests/cppunit/metadata_test.cc
@@ -20,7 +20,9 @@
 
 #include <gtest/gtest.h>
 
+#include <chrono>
 #include <memory>
+#include <thread>
 
 #include "storage/redis_metadata.h"
 #include "test_base.h"
@@ -90,7 +92,7 @@ TEST_F(RedisTypeTest, GetMetadata) {
   EXPECT_TRUE(s.ok() && fvs.size() == ret);
   HashMetadata metadata;
   std::string ns_key = redis_->AppendNamespacePrefix(key_);
-  s = redis_->GetMetadata(kRedisHash, ns_key, &metadata);
+  s = redis_->GetMetadata({kRedisHash}, ns_key, &metadata);
   EXPECT_EQ(fvs.size(), metadata.size);
   s = redis_->Del(key_);
   EXPECT_TRUE(s.ok());
@@ -114,6 +116,81 @@ TEST_F(RedisTypeTest, Expire) {
   s = redis_->Del(key_);
 }
 
+TEST_F(RedisTypeTest, ExpireTime) {
+  uint64_t ret = 0;
+  std::vector<FieldValue> fvs;
+  for (size_t i = 0; i < fields_.size(); i++) {
+    fvs.emplace_back(fields_[i].ToString(), values_[i].ToString());
+  }
+  rocksdb::Status s = hash_->MSet(key_, fvs, false, &ret);
+  EXPECT_TRUE(s.ok() && fvs.size() == ret);
+  int64_t now = 0;
+  rocksdb::Env::Default()->GetCurrentTime(&now);
+  uint64_t ms_offset = 2314;
+  uint64_t expire_timestamp_ms = now * 1000 + ms_offset;
+  s = redis_->Expire(key_, expire_timestamp_ms);
+  EXPECT_TRUE(s.ok());
+  uint64_t timestamp = 0;
+  s = redis_->GetExpireTime(key_, &timestamp);
+  EXPECT_TRUE(s.ok() && timestamp != 0);
+  if (METADATA_ENCODING_VERSION != 0) {
+    EXPECT_EQ(timestamp, expire_timestamp_ms);
+  } else {
+    EXPECT_EQ(timestamp, Metadata::ExpireMsToS(expire_timestamp_ms) * 1000);
+  }
+  s = redis_->Del(key_);
+}
+
+TEST_F(RedisTypeTest, ExpireTimeKeyNoExpireTime) {
+  uint64_t ret = 0;
+  std::vector<FieldValue> fvs;
+  for (size_t i = 0; i < fields_.size(); i++) {
+    fvs.emplace_back(fields_[i].ToString(), values_[i].ToString());
+  }
+  rocksdb::Status s = hash_->MSet(key_, fvs, false, &ret);
+  EXPECT_TRUE(s.ok() && fvs.size() == ret);
+  uint64_t timestamp = 0;
+  s = redis_->GetExpireTime(key_, &timestamp);
+  EXPECT_TRUE(s.ok() && timestamp == 0);
+  s = redis_->Del(key_);
+}
+
+TEST_F(RedisTypeTest, ExpireTimeKeyExpired) {
+  uint64_t ret = 0;
+  std::vector<FieldValue> fvs;
+  for (size_t i = 0; i < fields_.size(); i++) {
+    fvs.emplace_back(fields_[i].ToString(), values_[i].ToString());
+  }
+  rocksdb::Status s = hash_->MSet(key_, fvs, false, &ret);
+  EXPECT_TRUE(s.ok() && fvs.size() == ret);
+  int64_t now = 0;
+  rocksdb::Env::Default()->GetCurrentTime(&now);
+  uint64_t ms_offset = 1120;
+  uint64_t expire_timestamp_ms = now * 1000 + ms_offset;
+  s = redis_->Expire(key_, expire_timestamp_ms);
+  EXPECT_TRUE(s.ok());
+  std::this_thread::sleep_for(std::chrono::milliseconds(2000));
+  uint64_t timestamp = 0;
+  s = redis_->GetExpireTime(key_, &timestamp);
+  EXPECT_TRUE(s.IsNotFound() && timestamp == 0);
+  s = redis_->Del(key_);
+}
+
+TEST_F(RedisTypeTest, ExpireTimeKeyNotExisted) {
+  uint64_t ret = 0;
+  std::vector<FieldValue> fvs;
+  for (size_t i = 0; i < fields_.size(); i++) {
+    fvs.emplace_back(fields_[i].ToString(), values_[i].ToString());
+  }
+  rocksdb::Status s = hash_->MSet(key_, fvs, false, &ret);
+  EXPECT_TRUE(s.ok() && fvs.size() == ret);
+  uint64_t timestamp = 0;
+  s = redis_->GetExpireTime(key_ + "test", &timestamp);
+  EXPECT_TRUE(s.IsNotFound() && timestamp == 0);
+
+  s = redis_->Del(key_);
+}
+
 TEST(Metadata, MetadataDecodingBackwardCompatibleSimpleKey) {
   auto expire_at = (util::GetTimeStamp() + 10) * 1000;
   Metadata md_old(kRedisString, true, false);
diff --git a/tests/gocase/unit/expire/expire_test.go 
b/tests/gocase/unit/expire/expire_test.go
index 9986afaf..1a787f4e 100644
--- a/tests/gocase/unit/expire/expire_test.go
+++ b/tests/gocase/unit/expire/expire_test.go
@@ -71,6 +71,58 @@ func TestExpire(t *testing.T) {
                util.BetweenValues(t, rdb.TTL(ctx, "x").Val(), 13*time.Second, 
16*time.Second)
        })
 
+       t.Run("EXPIRETIME  - Check for EXPRIRETIME alike behavior", func(t 
*testing.T) {
+               require.NoError(t, rdb.Del(ctx, "x").Err())
+               require.NoError(t, rdb.Set(ctx, "x", "foo", 0).Err())
+
+               require.NoError(t, rdb.ExpireTime(ctx, "x").Err())
+               require.Equal(t, int64(-1), rdb.ExpireTime(ctx, 
"x").Val().Nanoseconds())
+
+               expireTime := time.Unix(time.Now().Unix()+1, 0)
+               require.NoError(t, rdb.ExpireAt(ctx, "x", 
time.Unix(time.Now().Unix()+1, 0)).Err())
+               require.NoError(t, rdb.ExpireTime(ctx, "x").Err())
+               require.Equal(t, expireTime.UnixMilli(), rdb.ExpireTime(ctx, 
"x").Val().Milliseconds())
+
+               require.NoError(t, rdb.ExpireTime(ctx, "x_key_no_exist").Err())
+               require.Equal(t, int64(-2), rdb.ExpireTime(ctx, 
"x_key_no_exist").Val().Nanoseconds())
+
+               time.Sleep(1001 * time.Millisecond)
+               require.NoError(t, rdb.ExpireTime(ctx, "x").Err())
+               require.Equal(t, int64(-2), rdb.ExpireTime(ctx, 
"x").Val().Nanoseconds())
+       })
+
+       t.Run("PEXPIRETIME  - Check for PEXPRIRETIME alike behavior", func(t 
*testing.T) {
+               require.NoError(t, rdb.Del(ctx, "x").Err())
+               require.NoError(t, rdb.Set(ctx, "x", "foo", 0).Err())
+
+               require.NoError(t, rdb.PExpireTime(ctx, "x").Err())
+               require.Equal(t, int64(-1), rdb.PExpireTime(ctx, 
"x").Val().Nanoseconds())
+
+               expireTime := time.Unix(time.Now().Unix()+1, 0)
+               require.NoError(t, rdb.PExpireAt(ctx, "x", expireTime).Err())
+               require.NoError(t, rdb.PExpireTime(ctx, "x").Err())
+               require.Equal(t, expireTime.UnixMilli(), rdb.PExpireTime(ctx, 
"x").Val().Milliseconds())
+
+               expireTime = time.UnixMilli(time.Now().Unix()*1000 + 1456)
+               require.NoError(t, rdb.PExpireAt(ctx, "x", expireTime).Err())
+               require.NoError(t, rdb.PExpireTime(ctx, "x").Err())
+               require.GreaterOrEqual(t, expireTime.UnixMilli(), 
rdb.PExpireTime(ctx, "x").Val().Milliseconds())
+               require.LessOrEqual(t, (expireTime.UnixMilli()+499)/1000*1000, 
rdb.PExpireTime(ctx, "x").Val().Milliseconds())
+
+               expireTime = time.UnixMilli(time.Now().Unix()*1000 + 1789)
+               require.NoError(t, rdb.PExpireAt(ctx, "x", expireTime).Err())
+               require.NoError(t, rdb.PExpireTime(ctx, "x").Err())
+               require.LessOrEqual(t, expireTime.UnixMilli(), 
rdb.PExpireTime(ctx, "x").Val().Milliseconds())
+               require.GreaterOrEqual(t, 
(expireTime.UnixMilli()+499)/1000*1000, rdb.PExpireTime(ctx, 
"x").Val().Milliseconds())
+
+               require.NoError(t, rdb.PExpireTime(ctx, "x_key_no_exist").Err())
+               require.Equal(t, int64(-2), rdb.PExpireTime(ctx, 
"x_key_no_exist").Val().Nanoseconds())
+
+               time.Sleep(2000 * time.Millisecond)
+               require.NoError(t, rdb.PExpireTime(ctx, "x").Err())
+               require.Equal(t, int64(-2), rdb.PExpireTime(ctx, 
"x").Val().Nanoseconds())
+       })
+
        t.Run("SETEX - Set + Expire combo operation. Check for TTL", func(t 
*testing.T) {
                require.NoError(t, rdb.SetEx(ctx, "x", "test", 
12*time.Second).Err())
                util.BetweenValues(t, rdb.TTL(ctx, "x").Val(), 10*time.Second, 
12*time.Second)

Reply via email to