Skip to content
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

Merged
merged 9 commits into from
Sep 25, 2024
Merged

fix: bitop do not add dst key if result is empty #3751

merged 9 commits into from
Sep 25, 2024

Conversation

kostasrim
Copy link
Contributor

@kostasrim kostasrim commented Sep 20, 2024

There are two bugs:

Bug 1:

bitop not does_not_exist tmp2
get does_not_exist

Should return nill and not ""

Bug 2:

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)

Bug 3:

hset foo bar val -> bitop not foo bar -> get foo returns WRONGTYPE

when get foo should return nill, that is bitop acts as a blind update similar to set command

resolves #3528

Signed-off-by: kostas <kostas@dragonflydb.io>
@kostasrim kostasrim requested a review from adiholden September 20, 2024 14:17
@kostasrim kostasrim self-assigned this Sep 20, 2024
@@ -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()) {
Copy link
Collaborator

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

Copy link
Contributor Author

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
Copy link
Collaborator

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

Copy link
Contributor Author

@kostasrim kostasrim Sep 23, 2024

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

@kostasrim kostasrim requested a review from adiholden September 23, 2024 09:21
post_updater_.Run();
if (new_value.empty()) {
// No need to run, it was a new entry and it got removed
post_updater_.Cancel();
Copy link
Collaborator

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

Copy link
Contributor Author

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 :(

@@ -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()) {
Copy link
Collaborator

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

Copy link
Contributor Author

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 ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should not

Copy link
Contributor Author

@kostasrim kostasrim Sep 23, 2024

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 😛

Copy link
Contributor Author

@kostasrim kostasrim Sep 23, 2024

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:

  1. on master do: hset foo bar val -> bitop not foo bar -> get foo returns WRONGTYPE
  2. on replica do GET foo returns ""

Now state_of(master) != state_of(replica)

Copy link
Contributor Author

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

@romange
Copy link
Collaborator

romange commented Sep 23, 2024

As a general remark - I suggest adding comments around points that raised discussions in the PR so that future selves could understand the intent

Signed-off-by: kostas <kostas@dragonflydb.io>
@kostasrim kostasrim requested a review from adiholden September 23, 2024 11:01
@kostasrim
Copy link
Contributor Author

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

await wait_available_async(c_replica)

c_master = master.client()
await c_master.execute_command(f"HSET foo bar val")
Copy link
Collaborator

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

Copy link
Collaborator

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

Copy link
Contributor Author

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

Copy link
Contributor Author

@kostasrim kostasrim Sep 23, 2024

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

await c_master.execute_command(f"BITOP not foo 1")

with pytest.raises(redis.exceptions.ResponseError):
await c_replica.execute_command(f"GET foo")
Copy link
Collaborator

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

@kostasrim kostasrim requested a review from adiholden September 23, 2024 13:28
@@ -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()
Copy link
Collaborator

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

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")
Copy link
Collaborator

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?

@kostasrim kostasrim requested a review from adiholden September 24, 2024 08:42
@kostasrim kostasrim merged commit 105c2bd into main Sep 25, 2024
12 checks passed
@kostasrim kostasrim deleted the kpr2 branch September 25, 2024 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

bitop not tmp1 tmp2 inconsistent result
3 participants