-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(hset_family): Add KEEPTTL support to HSetEx #4730
Conversation
fdf29dc
to
24067a5
Compare
6faf029
to
06c60e6
Compare
An optional DensePtr is passed to DenseSet::AddOrReplaceObj If set, this is used to avoid a search for existing object. A protected helper method is also added to DenseSet which returns the DensePtr and the associated raw pointer to caller. This is intended for use in the string map, where the DensePtr will be passed to AddOrReplaceObj, and the raw ptr will be used to fetch object expiry time. Signed-off-by: Abhijat Malviya <abhijat@dragonflydb.io>
06c60e6
to
3299f44
Compare
When calling AddOrUpdate in string map, if keepttl flag is passed, a search is performed for the field first. If the field is found, its ttl is used for future operations. Also the found object is passed to dense set DenseSet::AddOrReplaceObj to avoid extra searches. Signed-off-by: Abhijat Malviya <abhijat@dragonflydb.io>
3299f44
to
ef503f2
Compare
The KEEPTTL option is parsed along with NX and passed to the string map API. This option if specified makes sure that TTL is preserved for existing members.
Signed-off-by: Abhijat Malviya <abhijat@dragonflydb.io>
ef503f2
to
328c96b
Compare
src/core/string_map.cc
Outdated
bool keepttl) { | ||
void* dptr = nullptr; | ||
void* raw = nullptr; | ||
// If keepttl was specified, do a search. If the object is found pass it to AddOrReplaceObj | ||
// to avoid another search. We might end up doing two searches if keepttl is specified and | ||
// field is not present already. | ||
if (keepttl) { | ||
uint64_t hashcode = Hash(&field, 1); | ||
// dense ptr for AddOrReplaceObj and sds for getting expire time. | ||
std::tie(dptr, raw) = FindObjPtr(&field, hashcode, 1); | ||
|
||
if (raw) { | ||
if (const auto [val, val_ptr] = LoadValPtr(raw); val & kValTtlBit) { | ||
ttl_sec = GetExpiry(val_ptr); | ||
} | ||
} | ||
} | ||
|
||
// 8 additional bytes for a pointer to value. | ||
auto [newkey, sdsval_tag] = CreateEntry(field, value, time_now(), ttl_sec); | ||
|
||
// Replace the whole entry. | ||
sds prev_entry = (sds)AddOrReplaceObj(newkey, sdsval_tag & kValTtlBit); | ||
if (prev_entry) { | ||
if (sds prev_entry = (sds)AddOrReplaceObj(newkey, sdsval_tag & kValTtlBit, dptr); prev_entry) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I am missing something but I think we don't really need two searches
in any case since we can update the ttl field of dptr
directly. So why can't we simply do:
if (sds prev_entry = (sds)AddOrReplaceObj(newkey, sdsval_tag & kValTtlBit, dptr); prev_entry) {
// We know that there was a `prev_entry` so `keepttl` is relevant here.
if(keepttl) {
auto old_ttl = ObjExpireTime(prev_entry);
if(old_ttl != UINT_MAX) {
ObjUpdateExpireTime(dptr, old_ttl);
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I am missing something then:
- We don't need this search pass what found to AddOrReplaceObj dance.
- It works because
HSETEX
will always add a ttl field soCreateEntry
above will always "reserve" space for the ttl field anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all in all we can avoid this alltogether basically
Passing the result of search to AddOrReplaceObj as currently implemented in this PR to avoid two lookups
So there should be no extra overhead associated with the two searches
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To update the ttl
of dptr
after adding the object as you proposed, we need it to be initialized and pointing to the set member.
But we only get it from the first search added in this PR at
dragonfly/src/core/string_map.cc
Line 88 in 328c96b
std::tie(dptr, raw) = FindObjPtr(&field, hashcode, 1); |
If you compare with the main branch, we did not originally have access to dptr
in this method, so it wouldn't be possible to update the ttl without the first search.
I had also thought of adding a call to ObjUpdateExpireTime
inside DenseSet::AddOrReplaceObj
after we find the prev. entry, this way there is only one search and the code is much simpler (a flag has to be passed into AddOrReplaceObj to preserve ttl), something like the following in DenseSet::AddOrReplaceObj
:
void* res = dptr->Raw();
if (has_ttl && keepttl) {
if (const auto ttl = ObjExpireTime(res); ttl != UINT32_MAX) {
ObjUpdateExpireTime(res, ttl);
}
}
but that will change the behavior for all subclasses of DenseSet
instead of just the string map class. Maybe the keepttl
flag can be false by default so it doesn't change the behavior of other subclasses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dptr
was a typo. You don't need the denseptr
to update the object
. I will ping to explain internally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
made changes as discussed
src/server/hset_family.cc
Outdated
OpSetParams LoadHSetExParams(CmdArgParser& parser) { | ||
OpSetParams op_sp{}; | ||
|
||
auto res = parser.TryMapNext("NX"sv, HSetExOpts::NX, "KEEPTTL"sv, HSetExOpts::KEEPTTL); | ||
while (res.has_value()) { | ||
switch (res.value()) { | ||
case HSetExOpts::NX: | ||
op_sp.skip_if_exists = true; | ||
break; | ||
case HSetExOpts::KEEPTTL: | ||
op_sp.keepttl = true; | ||
break; | ||
} | ||
res = parser.TryMapNext("NX"sv, HSetExOpts::NX, "KEEPTTL"sv, HSetExOpts::KEEPTTL); | ||
} | ||
|
||
// Conversion errors are handled by caller | ||
op_sp.ttl = parser.Next<uint32_t>(); | ||
return op_sp; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need all of this ? Why not bool skip_if_exists = static_cast<bool>(parser.Check("KEEPTTL"sv));
similarly to what we have on line 729 ? (see skip_if_exists
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally tried with Check
but it seems to only work for a fixed order, eg if we have:
bool keepttl = parser.Check("KEEPTTL"sv);
bool skip = parser.Check("NX"sv);
This works if
- only
KEEPTTL
is passed - only
NX
is passed - both are passed in the exact order
KEEPTTL NX
But it rejects the command if we call HSETEX key NX KEEPTTL ...
, so we have the order hardcoded.
If we reverse the order of parser.Check
calls then NX KEEPTTL
will work but KEEPTTL NX
will not work.
So I used a loop so the current version of the code handles the options in any order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the order of options is guaranteed to be fixed eg NX
is always before KEEPTTL
then I can add the two parser.Check
calls in the same order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
switched to use Check
in a loop as discussed
Signed-off-by: Abhijat Malviya <abhijat@dragonflydb.io>
src/server/hset_family.cc
Outdated
while (true) { | ||
if (parser.Check("NX")) { | ||
op_sp.skip_if_exists = true; | ||
} else if (parser.Check("KEEPTTL")) { | ||
op_sp.keepttl = true; | ||
} else { | ||
break; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this again why don't we just enforce an order ? NX
should go first, followed by KEEPTTL
if it exists.
So we rewrite this as:
if (parser.Check("NX")) {
op_sp.skip_i_exists = true;
}
if (parser.Check("KEEPTTL")) {
op_sp.keepttl = true;
}
And we are done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
change to something similar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think this is a good decision to enforce the order. We dont do this in other commands afaik. This is actually more of a product decision of to enforce order of commands.
Please create a fix PR to not enforce the order @abhijat
IntArg(1)); | ||
EXPECT_EQ(Run({"HGET", "k", "afield"}), "aval"); | ||
// make afield expire | ||
AdvanceTime(1000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before we advance time
it would worth to assert that:
EXPECT_EQ(Run({"FIELDTTL", "k", "kttlfield"}).GetInt(), long_time);
EXPECT_EQ(Run({"FIELDTTL", "k", "afield"}).GetInt(), long_time);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added. Usually I'm a little wary of adding assertions where we check exact time values, but in this case I guess the test runs fast enough so it wouldn't fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't advance time, it will remain the same. We have full control over this :)
Signed-off-by: Abhijat Malviya <abhijat@dragonflydb.io>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🚢 🇮🇹 👨🍳
Signed-off-by: Abhijat Malviya <abhijat@dragonflydb.io>
Signed-off-by: Abhijat Malviya <abhijat@dragonflydb.io>
if (sds prev_entry = (sds)AddOrReplaceObj(newkey, sdsval_tag & kValTtlBit); prev_entry) { | ||
const bool prev_has_ttl = | ||
absl::little_endian::Load64(prev_entry + sdslen(prev_entry) + 1) & kValTtlBit; | ||
if (keepttl && prev_has_ttl) { | ||
SdsUpdateExpireTime(newkey, ObjExpireTime(prev_entry), 8); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Ouch. We had a DCHECK in ObjExpireTime
which we triggered right ? And then, ObjUpdateExpireTime
used now()
which changed the TTL. IMO I think the DCHECK in ObjExpireTime
is a mistake but let's keep it as is for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DCHECK
wasn't triggered earlier as we had no asserts which go through the code path where KEEPTTL
is passed but object has no expiry, so the object always had a valid expiry, the new assert added at the end of test now does check this code path.
Earlier what happened during test failure was:
- add object with
ttl=100
, expiry calculated to 103 (becausenow()
== 3) - add object again with
keepttl
, which ended up setting expiry to 103 in error. - check failed because
100 != FIELDTTL
|
||
// kttlfield is still present although with updated value | ||
EXPECT_EQ(Run({"HGET", "k", "kttlfield"}), "resetvalue"); | ||
EXPECT_EQ(Run({"FIELDTTL", "k", "kttlfield"}).GetInt(), 99); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now the math adds up. 100 second ttl, advance by 1 second. afield
expired and kttlfield
now requires 99
more seconds.
The
KEEPTTL
option is added toHSetEx
command.This option if specified makes sure that TTL is preserved for existing members. It coexists along with the
NX
option and should be specified at the start of the command:fixes #4701