diff --git a/src/server/generic_family.cc b/src/server/generic_family.cc index 8209d2b0a9ab..06b4fd626294 100644 --- a/src/server/generic_family.cc +++ b/src/server/generic_family.cc @@ -1325,6 +1325,38 @@ void GenericFamily::RenameNx(CmdArgList args, ConnectionContext* cntx) { } } +void GenericFamily::ExpireTime(CmdArgList args, ConnectionContext* cntx) { + ExpireTimeGeneric(args, cntx, TimeUnit::SEC); +} + +void GenericFamily::PExpireTime(CmdArgList args, ConnectionContext* cntx) { + ExpireTimeGeneric(args, cntx, TimeUnit::MSEC); +} + +void GenericFamily::ExpireTimeGeneric(CmdArgList args, ConnectionContext* cntx, TimeUnit unit) { + string_view key = ArgS(args, 0); + + auto cb = [&](Transaction* t, EngineShard* shard) { return OpExpireTime(t, shard, key); }; + OpResult result = cntx->transaction->ScheduleSingleHopT(std::move(cb)); + + if (result) { + long ttl = (unit == TimeUnit::SEC) ? (result.value() + 500) / 1000 : result.value(); + cntx->SendLong(ttl); + return; + } + + switch (result.status()) { + case OpStatus::KEY_NOTFOUND: + cntx->SendLong(-2); + break; + default: + LOG_IF(ERROR, result.status() != OpStatus::SKIPPED) + << "Unexpected status " << result.status(); + cntx->SendLong(-1); + break; + } +} + void GenericFamily::Ttl(CmdArgList args, ConnectionContext* cntx) { TtlGeneric(args, cntx, TimeUnit::SEC); } @@ -1487,7 +1519,8 @@ void GenericFamily::Scan(CmdArgList args, ConnectionContext* cntx) { } } -OpResult GenericFamily::OpTtl(Transaction* t, EngineShard* shard, string_view key) { +OpResult GenericFamily::OpExpireTime(Transaction* t, EngineShard* shard, + string_view key) { auto& db_slice = t->GetDbSlice(shard->shard_id()); auto [it, expire_it] = db_slice.FindReadOnly(t->GetDbContext(), key); if (!IsValid(it)) @@ -1496,11 +1529,23 @@ OpResult GenericFamily::OpTtl(Transaction* t, EngineShard* shard, stri if (!IsValid(expire_it)) return OpStatus::SKIPPED; - int64_t ttl_ms = db_slice.ExpireTime(expire_it) - t->GetDbContext().time_now_ms; + int64_t ttl_ms = db_slice.ExpireTime(expire_it); DCHECK_GT(ttl_ms, 0); // Otherwise FindReadOnly would return null. return ttl_ms; } +OpResult GenericFamily::OpTtl(Transaction* t, EngineShard* shard, string_view key) { + auto opExpireTimeResult = OpExpireTime(t, shard, key); + + if (opExpireTimeResult) { + int64_t ttl_ms = opExpireTimeResult.value() - t->GetDbContext().time_now_ms; + DCHECK_GT(ttl_ms, 0); // Otherwise FindReadOnly would return null. + return ttl_ms; + } else { + return opExpireTimeResult; + } +} + OpResult GenericFamily::OpExists(const OpArgs& op_args, const ShardArgs& keys) { DVLOG(1) << "Exists: " << keys.Front(); auto& db_slice = op_args.GetDbSlice(); @@ -1697,6 +1742,8 @@ constexpr uint32_t kStick = KEYSPACE | WRITE | FAST; constexpr uint32_t kSort = WRITE | SET | SORTEDSET | LIST | SLOW | DANGEROUS; constexpr uint32_t kMove = KEYSPACE | WRITE | FAST; constexpr uint32_t kRestore = KEYSPACE | WRITE | SLOW | DANGEROUS; +constexpr uint32_t kExpireTime = KEYSPACE | READ | FAST; +constexpr uint32_t kPExpireTime = KEYSPACE | READ | FAST; } // namespace acl void GenericFamily::Register(CommandRegistry* registry) { @@ -1738,7 +1785,9 @@ void GenericFamily::Register(CommandRegistry* registry) { << CI{"MOVE", CO::WRITE | CO::GLOBAL_TRANS | CO::NO_AUTOJOURNAL, 3, 1, 1, acl::kMove}.HFUNC( Move) << CI{"RESTORE", CO::WRITE, -4, 1, 1, acl::kRestore}.HFUNC(Restore) - << CI{"RANDOMKEY", CO::READONLY, 1, 0, 0, 0}.HFUNC(RandomKey); + << CI{"RANDOMKEY", CO::READONLY, 1, 0, 0, 0}.HFUNC(RandomKey) + << CI{"EXPIRETIME", CO::READONLY | CO::FAST, 2, 1, 1, acl::kExpireTime}.HFUNC(ExpireTime) + << CI{"PEXPIRETIME", CO::READONLY | CO::FAST, 2, 1, 1, acl::kPExpireTime}.HFUNC(PExpireTime); } } // namespace dfly diff --git a/src/server/generic_family.h b/src/server/generic_family.h index 411cc2535e2f..207d68009146 100644 --- a/src/server/generic_family.h +++ b/src/server/generic_family.h @@ -56,6 +56,8 @@ class GenericFamily { static void Rename(CmdArgList args, ConnectionContext* cntx); static void RenameNx(CmdArgList args, ConnectionContext* cntx); + static void ExpireTime(CmdArgList args, ConnectionContext* cntx); + static void PExpireTime(CmdArgList args, ConnectionContext* cntx); static void Ttl(CmdArgList args, ConnectionContext* cntx); static void Pttl(CmdArgList args, ConnectionContext* cntx); @@ -71,8 +73,11 @@ class GenericFamily { static ErrorReply RenameGeneric(CmdArgList args, bool destination_should_not_exist, ConnectionContext* cntx); + + static void ExpireTimeGeneric(CmdArgList args, ConnectionContext* cntx, TimeUnit unit); static void TtlGeneric(CmdArgList args, ConnectionContext* cntx, TimeUnit unit); + static OpResult OpExpireTime(Transaction* t, EngineShard* shard, std::string_view key); static OpResult OpTtl(Transaction* t, EngineShard* shard, std::string_view key); static OpResult OpRen(const OpArgs& op_args, std::string_view from, std::string_view to, bool destination_should_not_exist); diff --git a/src/server/generic_family_test.cc b/src/server/generic_family_test.cc index c749c1d38a6f..de61b9114d42 100644 --- a/src/server/generic_family_test.cc +++ b/src/server/generic_family_test.cc @@ -775,4 +775,19 @@ TEST_F(GenericFamilyTest, JsonType) { ASSERT_THAT(vec, ElementsAre("json")); } +TEST_F(GenericFamilyTest, ExpireTime) { + EXPECT_EQ(-2, CheckedInt({"EXPIRETIME", "foo"})); + EXPECT_EQ(-2, CheckedInt({"PEXPIRETIME", "foo"})); + Run({"set", "foo", "bar"}); + EXPECT_EQ(-1, CheckedInt({"EXPIRETIME", "foo"})); + EXPECT_EQ(-1, CheckedInt({"PEXPIRETIME", "foo"})); + + // set expiry + uint64_t expire_time_in_ms = TEST_current_time_ms + 5000; + uint64_t expire_time_in_seconds = (expire_time_in_ms + 500) / 1000; + Run({"pexpireat", "foo", absl::StrCat(expire_time_in_ms)}); + EXPECT_EQ(expire_time_in_seconds, CheckedInt({"EXPIRETIME", "foo"})); + EXPECT_EQ(expire_time_in_ms, CheckedInt({"PEXPIRETIME", "foo"})); +} + } // namespace dfly