mirror of
https://github.com/dragonflydb/dragonfly.git
synced 2025-05-10 18:05:44 +02:00
feat: Implement options for EXPIRE command (#2051)
Add NX, XX, LT, GT options to EXPIRE, EXPIREAT, PEXPIRE and PEXPIREAT Signed-off-by: Uku Loskit <ukuloskit@gmail.com>
This commit is contained in:
parent
eefd0c7808
commit
e9427dbbbc
5 changed files with 148 additions and 5 deletions
|
@ -9,6 +9,7 @@ extern "C" {
|
|||
}
|
||||
|
||||
#include "base/logging.h"
|
||||
#include "generic_family.h"
|
||||
#include "server/engine_shard_set.h"
|
||||
#include "server/journal/journal.h"
|
||||
#include "server/server_state.h"
|
||||
|
@ -732,9 +733,22 @@ OpResult<int64_t> DbSlice::UpdateExpire(const Context& cntx, PrimeIterator prime
|
|||
CHECK(Del(cntx.db_index, prime_it));
|
||||
return -1;
|
||||
} else if (IsValid(expire_it) && !params.persist) {
|
||||
auto current = ExpireTime(expire_it);
|
||||
if (params.expire_options & ExpireFlags::EXPIRE_NX) {
|
||||
return OpStatus::SKIPPED;
|
||||
}
|
||||
if ((params.expire_options & ExpireFlags::EXPIRE_LT) && current <= abs_msec) {
|
||||
return OpStatus::SKIPPED;
|
||||
} else if ((params.expire_options & ExpireFlags::EXPIRE_GT) && current >= abs_msec) {
|
||||
return OpStatus::SKIPPED;
|
||||
}
|
||||
|
||||
expire_it->second = FromAbsoluteTime(abs_msec);
|
||||
return abs_msec;
|
||||
} else {
|
||||
if (params.expire_options & ExpireFlags::EXPIRE_XX) {
|
||||
return OpStatus::SKIPPED;
|
||||
}
|
||||
AddExpire(cntx.db_index, prime_it, abs_msec);
|
||||
return abs_msec;
|
||||
}
|
||||
|
|
|
@ -97,6 +97,7 @@ class DbSlice {
|
|||
bool absolute = false;
|
||||
TimeUnit unit = TimeUnit::SEC;
|
||||
bool persist = false;
|
||||
int32_t expire_options = 0; // ExpireFlags
|
||||
|
||||
bool IsDefined() const {
|
||||
return persist || value > INT64_MIN;
|
||||
|
|
|
@ -734,6 +734,35 @@ void GenericFamily::Persist(CmdArgList args, ConnectionContext* cntx) {
|
|||
(*cntx)->SendLong(0);
|
||||
}
|
||||
|
||||
std::optional<int32_t> ParseExpireOptionsOrReply(const CmdArgList args, ConnectionContext* cntx) {
|
||||
int32_t flags = ExpireFlags::EXPIRE_ALWAYS;
|
||||
for (auto& arg : args) {
|
||||
ToUpper(&arg);
|
||||
auto arg_sv = ToSV(arg);
|
||||
if (arg_sv == "NX") {
|
||||
flags |= ExpireFlags::EXPIRE_NX;
|
||||
} else if (arg_sv == "XX") {
|
||||
flags |= ExpireFlags::EXPIRE_XX;
|
||||
} else if (arg_sv == "GT") {
|
||||
flags |= ExpireFlags::EXPIRE_GT;
|
||||
} else if (arg_sv == "LT") {
|
||||
flags |= ExpireFlags::EXPIRE_LT;
|
||||
} else {
|
||||
(*cntx)->SendError(absl::StrCat("Unsupported option: ", arg_sv));
|
||||
return nullopt;
|
||||
}
|
||||
}
|
||||
if ((flags & ExpireFlags::EXPIRE_NX) && (flags & ~ExpireFlags::EXPIRE_NX)) {
|
||||
(*cntx)->SendError("NX and XX, GT or LT options at the same time are not compatible");
|
||||
return nullopt;
|
||||
}
|
||||
if ((flags & ExpireFlags::EXPIRE_GT) && (flags & ExpireFlags::EXPIRE_LT)) {
|
||||
(*cntx)->SendError("GT and LT options at the same time are not compatible");
|
||||
return nullopt;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
void GenericFamily::Expire(CmdArgList args, ConnectionContext* cntx) {
|
||||
string_view key = ArgS(args, 0);
|
||||
string_view sec = ArgS(args, 1);
|
||||
|
@ -748,7 +777,11 @@ void GenericFamily::Expire(CmdArgList args, ConnectionContext* cntx) {
|
|||
}
|
||||
|
||||
int_arg = std::max<int64_t>(int_arg, -1);
|
||||
DbSlice::ExpireParams params{.value = int_arg};
|
||||
auto expire_options = ParseExpireOptionsOrReply(args.subspan(2), cntx);
|
||||
if (!expire_options) {
|
||||
return;
|
||||
}
|
||||
DbSlice::ExpireParams params{.value = int_arg, .expire_options = expire_options.value()};
|
||||
|
||||
auto cb = [&](Transaction* t, EngineShard* shard) {
|
||||
return OpExpire(t->GetOpArgs(shard), key, params);
|
||||
|
@ -768,7 +801,12 @@ void GenericFamily::ExpireAt(CmdArgList args, ConnectionContext* cntx) {
|
|||
}
|
||||
|
||||
int_arg = std::max<int64_t>(int_arg, 0L);
|
||||
DbSlice::ExpireParams params{.value = int_arg, .absolute = true};
|
||||
auto expire_options = ParseExpireOptionsOrReply(args.subspan(2), cntx);
|
||||
if (!expire_options) {
|
||||
return;
|
||||
}
|
||||
DbSlice::ExpireParams params{
|
||||
.value = int_arg, .absolute = true, .expire_options = expire_options.value()};
|
||||
|
||||
auto cb = [&](Transaction* t, EngineShard* shard) {
|
||||
return OpExpire(t->GetOpArgs(shard), key, params);
|
||||
|
@ -812,7 +850,14 @@ void GenericFamily::PexpireAt(CmdArgList args, ConnectionContext* cntx) {
|
|||
return (*cntx)->SendError(kInvalidIntErr);
|
||||
}
|
||||
int_arg = std::max<int64_t>(int_arg, 0L);
|
||||
DbSlice::ExpireParams params{.value = int_arg, .absolute = true, .unit = TimeUnit::MSEC};
|
||||
auto expire_options = ParseExpireOptionsOrReply(args.subspan(2), cntx);
|
||||
if (!expire_options) {
|
||||
return;
|
||||
}
|
||||
DbSlice::ExpireParams params{.value = int_arg,
|
||||
.absolute = true,
|
||||
.unit = TimeUnit::MSEC,
|
||||
.expire_options = expire_options.value()};
|
||||
|
||||
auto cb = [&](Transaction* t, EngineShard* shard) {
|
||||
return OpExpire(t->GetOpArgs(shard), key, params);
|
||||
|
@ -835,7 +880,12 @@ void GenericFamily::Pexpire(CmdArgList args, ConnectionContext* cntx) {
|
|||
return (*cntx)->SendError(kInvalidIntErr);
|
||||
}
|
||||
int_arg = std::max<int64_t>(int_arg, 0L);
|
||||
DbSlice::ExpireParams params{.value = int_arg, .unit = TimeUnit::MSEC};
|
||||
auto expire_options = ParseExpireOptionsOrReply(args.subspan(2), cntx);
|
||||
if (!expire_options) {
|
||||
return;
|
||||
}
|
||||
DbSlice::ExpireParams params{
|
||||
.value = int_arg, .unit = TimeUnit::MSEC, .expire_options = expire_options.value()};
|
||||
|
||||
auto cb = [&](Transaction* t, EngineShard* shard) {
|
||||
return OpExpire(t->GetOpArgs(shard), key, params);
|
||||
|
@ -1522,7 +1572,7 @@ void GenericFamily::Register(CommandRegistry* registry) {
|
|||
<< CI{"ECHO", CO::LOADING | CO::FAST, 2, 0, 0, 0, acl::kEcho}.HFUNC(Echo)
|
||||
<< CI{"EXISTS", CO::READONLY | CO::FAST, -2, 1, -1, 1, acl::kExists}.HFUNC(Exists)
|
||||
<< CI{"TOUCH", CO::READONLY | CO::FAST, -2, 1, -1, 1, acl::kTouch}.HFUNC(Exists)
|
||||
<< CI{"EXPIRE", CO::WRITE | CO::FAST | CO::NO_AUTOJOURNAL, 3, 1, 1, 1, acl::kExpire}.HFUNC(
|
||||
<< CI{"EXPIRE", CO::WRITE | CO::FAST | CO::NO_AUTOJOURNAL, -3, 1, 1, 1, acl::kExpire}.HFUNC(
|
||||
Expire)
|
||||
<< CI{"EXPIREAT", CO::WRITE | CO::FAST | CO::NO_AUTOJOURNAL, 3, 1, 1, 1, acl::kExpireAt}
|
||||
.HFUNC(ExpireAt)
|
||||
|
|
|
@ -24,6 +24,14 @@ class ConnectionContext;
|
|||
class CommandRegistry;
|
||||
class EngineShard;
|
||||
|
||||
enum ExpireFlags {
|
||||
EXPIRE_ALWAYS = 0,
|
||||
EXPIRE_NX = 1 << 0, // Set expiry only when key has no expiry
|
||||
EXPIRE_XX = 1 << 2, // Set expiry only when the key has expiry
|
||||
EXPIRE_GT = 1 << 3, // GT: Set expiry only when the new expiry is greater than current one
|
||||
EXPIRE_LT = 1 << 4, // LT: Set expiry only when the new expiry is less than current one
|
||||
};
|
||||
|
||||
class GenericFamily {
|
||||
public:
|
||||
static void Init(util::ProactorPool* pp);
|
||||
|
|
|
@ -75,6 +75,76 @@ TEST_F(GenericFamilyTest, Expire) {
|
|||
EXPECT_THAT(resp, ArgType(RespExpr::NIL));
|
||||
}
|
||||
|
||||
TEST_F(GenericFamilyTest, ExpireOptions) {
|
||||
// NX and XX are mutually exclusive
|
||||
Run({"set", "key", "val"});
|
||||
auto resp = Run({"expire", "key", "3600", "NX", "XX"});
|
||||
ASSERT_THAT(resp, ErrArg("NX and XX, GT or LT options at the same time are not compatible"));
|
||||
|
||||
// NX and GT are mutually exclusive
|
||||
resp = Run({"expire", "key", "3600", "NX", "GT"});
|
||||
ASSERT_THAT(resp, ErrArg("NX and XX, GT or LT options at the same time are not compatible"));
|
||||
|
||||
// NX and LT are mutually exclusive
|
||||
resp = Run({"expire", "key", "3600", "NX", "LT"});
|
||||
ASSERT_THAT(resp, ErrArg("NX and XX, GT or LT options at the same time are not compatible"));
|
||||
|
||||
// GT and LT are mutually exclusive
|
||||
resp = Run({"expire", "key", "3600", "GT", "LT"});
|
||||
ASSERT_THAT(resp, ErrArg("GT and LT options at the same time are not compatible"));
|
||||
|
||||
// NX option should be added since there is no expiry
|
||||
resp = Run({"expire", "key", "3600", "NX"});
|
||||
EXPECT_THAT(resp, IntArg(1));
|
||||
resp = Run({"ttl", "key"});
|
||||
EXPECT_THAT(resp.GetInt(), 3600);
|
||||
|
||||
// running again with NX option, should not change expiry
|
||||
resp = Run({"expire", "key", "42", "NX"});
|
||||
EXPECT_THAT(resp, IntArg(0));
|
||||
|
||||
// given a key with no expiry
|
||||
Run({"set", "key2", "val"});
|
||||
resp = Run({"expire", "key2", "404", "XX"});
|
||||
// XX does not apply expiry since key has no existing expiry
|
||||
EXPECT_THAT(resp, IntArg(0));
|
||||
resp = Run({"ttl", "key2"});
|
||||
EXPECT_THAT(resp.GetInt(), -1);
|
||||
|
||||
// set expiry to 101
|
||||
resp = Run({"expire", "key", "101"});
|
||||
EXPECT_THAT(resp, IntArg(1));
|
||||
|
||||
// GT should not apply expiry since new is not greater than the current one
|
||||
resp = Run({"expire", "key", "100", "GT"});
|
||||
EXPECT_THAT(resp, IntArg(0));
|
||||
resp = Run({"ttl", "key"});
|
||||
EXPECT_THAT(resp.GetInt(), 101);
|
||||
|
||||
// GT should apply expiry since new is greater than the current one
|
||||
resp = Run({"expire", "key", "102", "GT"});
|
||||
EXPECT_THAT(resp, IntArg(1));
|
||||
resp = Run({"ttl", "key"});
|
||||
EXPECT_THAT(resp.GetInt(), 102);
|
||||
|
||||
// GT should not apply since expiry is smaller than current
|
||||
resp = Run({"expire", "key", "101", "GT"});
|
||||
EXPECT_THAT(resp, IntArg(0));
|
||||
resp = Run({"ttl", "key"});
|
||||
EXPECT_THAT(resp.GetInt(), 102);
|
||||
|
||||
// LT should apply new expiry is smaller than current
|
||||
resp = Run({"expire", "key", "101", "LT"});
|
||||
EXPECT_THAT(resp, IntArg(1));
|
||||
resp = Run({"ttl", "key"});
|
||||
EXPECT_THAT(resp.GetInt(), 101);
|
||||
|
||||
resp = Run({"expire", "key", "102", "LT"});
|
||||
EXPECT_THAT(resp, IntArg(0));
|
||||
resp = Run({"ttl", "key"});
|
||||
EXPECT_THAT(resp.GetInt(), 101);
|
||||
}
|
||||
|
||||
TEST_F(GenericFamilyTest, Del) {
|
||||
for (size_t i = 0; i < 1000; ++i) {
|
||||
Run({"set", StrCat("foo", i), "1"});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue