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:
Uku Loskit 2023-10-25 11:26:33 +03:00 committed by GitHub
parent eefd0c7808
commit e9427dbbbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 148 additions and 5 deletions

View file

@ -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;
}

View file

@ -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;

View file

@ -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)

View file

@ -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);

View file

@ -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"});