-
Notifications
You must be signed in to change notification settings - Fork 981
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
fix: bitop do not add dst key if result is empty #3751
Conversation
Signed-off-by: kostas <kostas@dragonflydb.io>
src/server/bitops_family.cc
Outdated
@@ -343,8 +343,14 @@ std::string ElementAccess::Value() const { | |||
|
|||
void ElementAccess::Commit(std::string_view new_value) const { | |||
if (shard_) { | |||
element_iter_->second.SetString(new_value); | |||
post_updater_.Run(); | |||
if (new_value.empty() && IsNewEntry()) { |
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 think we should delete it even if its not a new entry
set x y
bitop not x key
get x
should also return nil
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 also added a test and confirmed the behaviour with Valkey. good catch
@@ -1159,7 +1165,7 @@ void BitOp(CmdArgList args, ConnectionContext* cntx) { | |||
cntx->transaction->Execute(std::move(shard_bitop), false); // we still have more work to do | |||
// All result from each shard | |||
const auto joined_results = CombineResultOp(result_set, op); | |||
// Second phase - save to targe key if successful | |||
// Second phase - save to target key if successful |
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.
check the replication journal code line 1185. need to fix this also
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.
Wow. I blindly did not bother with the journal because I thought that we replicate the same command always
. But in this context, we do not. What we do instead is we take the result of the operation and call SET
directly which is a very nice
optimization since we don't now need to run the command on the replica but rather do an update
on the result directly.
This is a nice pattern/optimization to know. +1
src/server/bitops_family.cc
Outdated
post_updater_.Run(); | ||
if (new_value.empty()) { | ||
// No need to run, it was a new entry and it got removed | ||
post_updater_.Cancel(); |
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 think you should run the post updater here as this might be not a new 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.
yep +1. I wish we could run it unconditionally though (but we can't because we have an assertion within it :(
src/server/bitops_family.cc
Outdated
@@ -1176,7 +1182,15 @@ void BitOp(CmdArgList args, ConnectionContext* cntx) { | |||
} | |||
|
|||
if (shard->journal()) { | |||
RecordJournal(t->GetOpArgs(shard), "SET", {dest_key, op_result}); | |||
if (op_result.empty()) { |
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.
also if find_res == OpStatus::OK
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.
my question here would be if the result is not ok why do we RecordJournal
?
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.
we should not
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.
we had another bug then even before my changes 😛
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.
setup master and replica and then:
- on master do:
hset foo bar val
->bitop not foo bar
->get foo
returns WRONGTYPE - on replica do
GET foo
returns ""
Now state_of(master) != state_of(replica)
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 added this on the issue + supplied the fix + added a replication test case so we don't break it again
As a general remark - I suggest adding comments around points that raised discussions in the PR so that future selves could understand the intent |
yep +1. I added a comment + the tests |
tests/dragonfly/replication_test.py
Outdated
await wait_available_async(c_replica) | ||
|
||
c_master = master.client() | ||
await c_master.execute_command(f"HSET foo bar val") |
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 would add this as a subtest case of def test_rewrites
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.
you will check there that del foo was sent
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.
hmm I did not know that bitops
acts as a blind update. I tested this with Valkey, so if the operation returns WRONG_TYPE
we should still delete the key
. I will add this to the logic + comment + move this to rewrite
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.
So the fix above is also incorrect if (find_res == OpStatus::OK)
!
I will add a unit for this as well
tests/dragonfly/replication_test.py
Outdated
await c_master.execute_command(f"BITOP not foo 1") | ||
|
||
with pytest.raises(redis.exceptions.ResponseError): | ||
await c_replica.execute_command(f"GET foo") |
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.
also this is not very robust because you dont know if replica already got the journal change or not, it could return error because foo is not a string and not because it was deleted
tests/dragonfly/replication_test.py
Outdated
@@ -673,6 +673,18 @@ async def check_expire(key): | |||
await skip_cmd() | |||
# Check BITOP turns into SET | |||
await check("BITOP OR kdest k1 k2", r"SET kdest 1100") | |||
c_master = master.client() |
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.
why do you need this line
tests/dragonfly/replication_test.py
Outdated
await c_master.execute_command(f"HSET foo bar val") | ||
await skip_cmd() | ||
await check("BITOP NOT foo tmp", r"DEL foo") | ||
await c_master.execute_command(f"DEL foo") |
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.
does foo exists in master at this point? why do you need del foo?
There are two bugs:
Bug 1:
Should return
nill
and not""
Bug 2:
Bug 3:
when
get foo
should returnnill
, that isbitop
acts as ablind update
similar toset
commandresolves #3528