Skip to content

Commit 611e9b3

Browse files
authored
feat(hset_family): Add KEEPTTL support to HSetEx (#4730)
* feat(hset_family): Add support for KEEPTTL to HSETEX The KEEPTTL option if specified makes sure that TTL is preserved for existing members. Signed-off-by: Abhijat Malviya <[email protected]>
1 parent 4f70d1b commit 611e9b3

File tree

5 files changed

+64
-14
lines changed

5 files changed

+64
-14
lines changed

src/core/dense_set.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,8 +690,8 @@ void* DenseSet::PopInternal() {
690690

691691
void* DenseSet::AddOrReplaceObj(void* obj, bool has_ttl) {
692692
uint64_t hc = Hash(obj, 0);
693-
DensePtr* dptr = entries_.empty() ? nullptr : Find(obj, BucketId(hc), 0).second;
694693

694+
DensePtr* dptr = entries_.empty() ? nullptr : Find(obj, BucketId(hc), 0).second;
695695
if (dptr) { // replace existing object.
696696
// A bit confusing design: ttl bit is located on the wrapping pointer,
697697
// therefore we must set ttl bit before unrapping below.

src/core/string_map.cc

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,18 @@ StringMap::~StringMap() {
6565
Clear();
6666
}
6767

68-
bool StringMap::AddOrUpdate(string_view field, string_view value, uint32_t ttl_sec) {
68+
bool StringMap::AddOrUpdate(std::string_view field, std::string_view value, uint32_t ttl_sec,
69+
bool keepttl) {
6970
// 8 additional bytes for a pointer to value.
7071
auto [newkey, sdsval_tag] = CreateEntry(field, value, time_now(), ttl_sec);
7172

7273
// Replace the whole entry.
73-
sds prev_entry = (sds)AddOrReplaceObj(newkey, sdsval_tag & kValTtlBit);
74-
if (prev_entry) {
74+
if (sds prev_entry = (sds)AddOrReplaceObj(newkey, sdsval_tag & kValTtlBit); prev_entry) {
75+
const bool prev_has_ttl =
76+
absl::little_endian::Load64(prev_entry + sdslen(prev_entry) + 1) & kValTtlBit;
77+
if (keepttl && prev_has_ttl) {
78+
SdsUpdateExpireTime(newkey, ObjExpireTime(prev_entry), 8);
79+
}
7580
ObjDelete(prev_entry, false);
7681
return false;
7782
}

src/core/string_map.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ class StringMap : public DenseSet {
104104
};
105105

106106
// otherwise updates its value and returns false.
107-
bool AddOrUpdate(std::string_view field, std::string_view value, uint32_t ttl_sec = UINT32_MAX);
107+
bool AddOrUpdate(std::string_view field, std::string_view value, uint32_t ttl_sec = UINT32_MAX,
108+
bool keepttl = false);
108109

109110
// Returns true if field was added
110111
// false, if already exists. In that case no update is done.

src/server/hset_family.cc

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ OpResult<size_t> OpStrLen(const OpArgs& op_args, string_view key, string_view fi
607607
struct OpSetParams {
608608
bool skip_if_exists = false;
609609
uint32_t ttl = UINT32_MAX;
610+
bool keepttl = false;
610611
};
611612

612613
OpResult<uint32_t> OpSet(const OpArgs& op_args, string_view key, CmdArgList values,
@@ -673,7 +674,7 @@ OpResult<uint32_t> OpSet(const OpArgs& op_args, string_view key, CmdArgList valu
673674
if (op_sp.skip_if_exists)
674675
added = sm->AddOrSkip(field, value, op_sp.ttl);
675676
else
676-
added = sm->AddOrUpdate(field, value, op_sp.ttl);
677+
added = sm->AddOrUpdate(field, value, op_sp.ttl, op_sp.keepttl);
677678

678679
created += unsigned(added);
679680
}
@@ -720,19 +721,23 @@ OpResult<vector<long>> OpHExpire(const OpArgs& op_args, string_view key, uint32_
720721
return HSetFamily::SetFieldsExpireTime(op_args, ttl_sec, key, values, pv);
721722
}
722723

723-
// HSETEX key [NX] tll_sec field value field value ...
724+
// HSETEX key [NX] [KEEPTTL] tll_sec field value field value ...
724725
void HSetEx(CmdArgList args, const CommandContext& cmd_cntx) {
725726
CmdArgParser parser{args};
726727

727728
string_view key = parser.Next();
729+
OpSetParams op_sp;
728730

729-
bool skip_if_exists = static_cast<bool>(parser.Check("NX"sv));
730-
string_view ttl_str = parser.Next();
731+
op_sp.skip_if_exists = parser.Check("NX");
732+
op_sp.keepttl = parser.Check("KEEPTTL");
733+
op_sp.ttl = parser.Next<uint32_t>();
731734

732-
uint32_t ttl_sec;
733-
constexpr uint32_t kMaxTtl = (1UL << 26);
735+
if (parser.HasError()) {
736+
return cmd_cntx.rb->SendError(parser.Error()->MakeReply());
737+
}
734738

735-
if (!absl::SimpleAtoi(ttl_str, &ttl_sec) || ttl_sec == 0 || ttl_sec > kMaxTtl) {
739+
constexpr uint32_t kMaxTtl = (1UL << 26);
740+
if (op_sp.ttl == 0 || op_sp.ttl > kMaxTtl) {
736741
return cmd_cntx.rb->SendError(kInvalidIntErr);
737742
}
738743

@@ -743,8 +748,6 @@ void HSetEx(CmdArgList args, const CommandContext& cmd_cntx) {
743748
kSyntaxErrType);
744749
}
745750

746-
OpSetParams op_sp{skip_if_exists, ttl_sec};
747-
748751
auto cb = [&](Transaction* t, EngineShard* shard) {
749752
return OpSet(t->GetOpArgs(shard), key, fields, op_sp);
750753
};

src/server/hset_family_test.cc

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,47 @@ TEST_F(HSetFamilyTest, HSetEx) {
364364
resp = Run({"HGET", "k", "field4"});
365365
EXPECT_THAT(resp,
366366
"value"); // HSETEX with NX option; old expiration time was NOT replaced by a new one
367+
368+
// KEEPTTL related asserts
369+
EXPECT_THAT(Run({"HSETEX", "k", long_time, "kttlfield", "value"}), IntArg(1));
370+
EXPECT_EQ(Run({"HGET", "k", "kttlfield"}), "value");
371+
EXPECT_EQ(CheckedInt({"FIELDTTL", "k", "kttlfield"}), 100);
372+
373+
// KEEPTTL resets value of kttlfield, but preserves its TTL. afield is added with TTL=1
374+
EXPECT_THAT(Run({"HSETEX", "k", "KEEPTTL", "1", "kttlfield", "resetvalue", "afield", "aval"}),
375+
IntArg(1));
376+
EXPECT_EQ(CheckedInt({"FIELDTTL", "k", "kttlfield"}), 100);
377+
EXPECT_EQ(Run({"FIELDTTL", "k", "afield"}).GetInt(), 1);
378+
EXPECT_EQ(Run({"HGET", "k", "afield"}), "aval");
379+
// make afield expire
380+
AdvanceTime(1000);
381+
EXPECT_THAT(Run({"HGET", "k", "afield"}), ArgType(RespExpr::NIL));
382+
383+
// kttlfield is still present although with updated value
384+
EXPECT_EQ(Run({"HGET", "k", "kttlfield"}), "resetvalue");
385+
EXPECT_EQ(Run({"FIELDTTL", "k", "kttlfield"}).GetInt(), 99);
386+
387+
// If NX is supplied, with or without KEEPTTL neither expiry nor value is updated
388+
EXPECT_THAT(Run({"HSETEX", "k", "NX", "KEEPTTL", "1", "kttlfield", "value"}), IntArg(0));
389+
390+
// No updates
391+
EXPECT_EQ(Run({"HGET", "k", "kttlfield"}), "resetvalue");
392+
EXPECT_EQ(Run({"FIELDTTL", "k", "kttlfield"}).GetInt(), 99);
393+
394+
EXPECT_THAT(Run({"HSETEX", "k", "NX", "1", "kttlfield", "value"}), IntArg(0));
395+
// No updates
396+
EXPECT_EQ(Run({"HGET", "k", "kttlfield"}), "resetvalue");
397+
EXPECT_EQ(Run({"FIELDTTL", "k", "kttlfield"}).GetInt(), 99);
398+
399+
// Invalid TTL handling
400+
EXPECT_THAT(Run({"HSETEX", "k", "NX", "zero", "kttlfield", "value"}),
401+
ErrArg("ERR value is not an integer or out of range"));
402+
403+
// Exercise the code path where a field is added without TTL, but then we set a new expiration AND
404+
// provide KEEPTTL. Since there was no old expiry, the new TTL should be applied.
405+
EXPECT_EQ(Run({"HSET", "k", "nottl", "val"}), 1);
406+
EXPECT_EQ(Run({"HSETEX", "k", "KEEPTTL", long_time, "nottl", "newval"}), 0);
407+
EXPECT_EQ(Run({"FIELDTTL", "k", "nottl"}).GetInt(), 100);
367408
}
368409

369410
TEST_F(HSetFamilyTest, TriggerConvertToStrMap) {

0 commit comments

Comments
 (0)