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

Sort out the mess around writable replicas and lookupKeyRead/Write #9572

Merged
merged 24 commits into from Nov 28, 2021

Conversation

zuiderkwast
Copy link
Contributor

@zuiderkwast zuiderkwast commented Sep 30, 2021

Writable replicas now no longer use the values of expired keys. Expired keys are
deleted when lookupKeyWrite() is used, even on a writable replica. Previously,
writable replicas could use the value of an expired key in write commands such
as INCR, SUNIONSTORE, etc..

This commit also sorts out the mess around the functions lookupKeyRead() and
lookupKeyWrite() so they now indicate what we intend to do with the key and
are not affected by the command calling them.

Multi-key commands like SUNIONSTORE, ZUNIONSTORE, COPY and SORT with the
store option now use lookupKeyRead() for the keys they're reading from (which will
not allow reading from logically expired keys).

This commit also fixes a bug where PFCOUNT could return a value of an
expired key.

Test modules commands have their readonly and write flags updated to correctly
reflect their lookups for reading or writing. Modules are not required to
correctly reflect this in their command flags, but this change is made for
consistency since the tests serve as usage examples.

Fixes #6842. Fixes #7475.

src/db.c Outdated Show resolved Hide resolved
src/db.c Outdated Show resolved Hide resolved
src/db.c Show resolved Hide resolved
src/sort.c Outdated Show resolved Hide resolved
src/sort.c Outdated Show resolved Hide resolved
@oranagra oranagra added this to Backlog in 7.0 via automation Oct 10, 2021
@oranagra oranagra moved this from Backlog to In progress in 7.0 Oct 10, 2021
Copy link
Member

@oranagra oranagra left a comment

Choose a reason for hiding this comment

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

i'd like to add quite a few tests.
the obvious ones are the bugs mentioned in #6842, but probably quite a few other tests that verify the intended behavior so it won't be accidentally broken in the future.

src/db.c Show resolved Hide resolved
src/db.c Outdated Show resolved Hide resolved
src/db.c Outdated Show resolved Hide resolved
src/db.c Outdated Show resolved Hide resolved
src/db.c Outdated Show resolved Hide resolved
src/sort.c Outdated Show resolved Hide resolved
zuiderkwast and others added 9 commits October 14, 2021 08:35
Writable replicas now no longer use the data of expired keys. Expired keys
are deleted when lookupKeyWrite() is used, even on a writable replica.

This commit also sorts out the mess around the commands lookupKeyRead() and
lookupKeyWrite() so they now indicate what we intend to do with the key and
are not be affected by the command itself.

Multi-key commands like sunionstore, zinterstore, copy and sort with the
store option now use lookupKeyRead() for the keys they're reading from, but
with flags preserving the legacy behaviour (not touching keyspace hits/misses
counters, etc.).
Co-authored-by: yoav-steinberg <yoav@monfort.co.il>
Set current_client to AOF client during AOF loading.

Add an assert forbidding the WRITE flag in lookupKeyReadWithFlags.

Extra: Don't touch keys stats and LRU when determining ASK redirect.
@oranagra
Copy link
Member

@zuiderkwast i think you should avoid doing git rebase and push -f, just stick to merge and incremental commits.
it's hard to keep track of what's new and what was already reviewed, and since we're gonna squash-merge this anyway, there's no real need for modifying existing commits.

src/hyperloglog.c Show resolved Hide resolved
tests/integration/replication-3.tcl Outdated Show resolved Hide resolved
tests/integration/replication-3.tcl Outdated Show resolved Hide resolved
src/db.c Outdated Show resolved Hide resolved
tests/integration/replication-3.tcl Outdated Show resolved Hide resolved
src/server.h Outdated Show resolved Hide resolved
tests/integration/replication-3.tcl Outdated Show resolved Hide resolved
@oranagra
Copy link
Member

@redis/core-team not sure if we need a major decision for this, but since it's a delicate subject, i'd love for you to review.

@oranagra
Copy link
Member

there is a way to check if the key exists without deleting it (DEBUG OBJECT).
but i don't see why there's a race...
the Tcl code has after 100 so we know at least 100ms passed since we got the reply from PEXPIRE.
this means that when we execute SWAPDB, the check in it that tests the expiration time will surely find that it already expired.
maybe there's an off by one issue, i.e. > 100 vs >= 100, so when the test is super fast is fails.
if that's the case, we can sleep for 101, but i don't see any other explanation.
please look into it and then we'll merge.

@oranagra oranagra merged commit acf3495 into redis:unstable Nov 28, 2021
@oranagra oranagra moved this from Awaits merge to Done in 7.0 Nov 28, 2021
@zuiderkwast zuiderkwast deleted the writable-replicas branch November 28, 2021 10:40
}
}

test {Replication of an expired key does not delete the expired key} {
Copy link
Collaborator

Choose a reason for hiding this comment

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

https://github.com/enjoy-binbin/redis/runs/4440834343?check_suite_focus=true#step:4:5814

the test failed once on my CI (FreeBSD), looks like was a timing issue.
I took a look, here are my thoughts:

  • the key expired before INCR was executed, because the execution time of wait_for_ofs_sync exceeded one second. (or maybe related the KILL).

I didn't think of a good way:

  1. maybe we can use $slave debug sleep ? (But there are also such problems)
  2. add a retry

Check that k is locigally expired but is present in the replica.

also there was a typo (locigally)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Thanks for looking into it.

I don't understand how debug sleep can help...?

Add a retry, yes I guess it can work. We can double the expire time at every retry, so starting with 1 second, then 2, 4, 8 etc. We can add a check before the first wait_for_ofs_sync that k didn't expire and if it did, we retry. WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

oh i mean that maybe we can use debug sleep to replace the kill

as for retry. The double is a good idea. Maybe we can start from Ms, reduce the running time.

edit: #11548

hwware pushed a commit to hwware/redis that referenced this pull request Dec 20, 2021
…edis#9572)

Writable replicas now no longer use the values of expired keys. Expired keys are
deleted when lookupKeyWrite() is used, even on a writable replica. Previously,
writable replicas could use the value of an expired key in write commands such
as INCR, SUNIONSTORE, etc..

This commit also sorts out the mess around the functions lookupKeyRead() and
lookupKeyWrite() so they now indicate what we intend to do with the key and
are not affected by the command calling them.

Multi-key commands like SUNIONSTORE, ZUNIONSTORE, COPY and SORT with the
store option now use lookupKeyRead() for the keys they're reading from (which will
not allow reading from logically expired keys).

This commit also fixes a bug where PFCOUNT could return a value of an
expired key.

Test modules commands have their readonly and write flags updated to correctly
reflect their lookups for reading or writing. Modules are not required to
correctly reflect this in their command flags, but this change is made for
consistency since the tests serve as usage examples.

Fixes redis#6842. Fixes redis#7475.
* shall not be used in readonly commands. Modules are accepted so
* that we don't break old modules. */
client *c = server.in_eval ? server.lua_client : server.current_client;
serverAssert(!c || !c->cmd || (c->cmd->flags & (CMD_WRITE|CMD_MODULE)));
Copy link

Choose a reason for hiding this comment

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

this assert assume that opening a key for write can’t be called from a redis command
but in RedisGraph when a SAVE command end we delete the temporary keys we created during the save process
we need to be able to workaround this assertion
this crashes redis in our tests
stack trace example:

------ STACK TRACE ------                                                                  
                                                                                           
Backtrace:                                                                                 
0   redis-server                        0x000000010cad474a lookupKey.cold.1 + 26           
1   redis-server                        0x000000010c9f1902 lookupKey + 402                 
2   redis-server                        0x000000010ca6b704 RM_OpenKey + 68                 
3   redisgraph.so                       0x000000010d3391f2 _DeleteGraphMetaKeys + 194      
4   redisgraph.so                       0x000000010d339392 _ClearKeySpaceMetaKeys + 88     
5   redisgraph.so                       0x000000010d339597 _PersistenceEventHandler + 202  
6   redis-server                        0x000000010ca74b93 moduleFireServerEvent + 195     
7   redis-server                        0x000000010ca03fd1 rdbSave + 641                   
8   redis-server                        0x000000010ca08eaa saveCommand + 218               
9   redis-server                        0x000000010c9cddd6 call + 278                      
10  redis-server                        0x000000010c9cef08 processCommand + 2904   

Copy link
Member

Choose a reason for hiding this comment

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

@zuiderkwast we didn't want to break modules, and assumed that c->cmd will be a module command, but with notifications and events, that could be done from other random contexts.

IIRC this assertion was just there in order to help us find native redis commands that are not flagged correctly.
it could have been sufficient to test that we're not on a writable-replica, but we thought that coverage for such a test will be low, and preferred to check our assumption on masters too.

As far as i can tell, our options now are:

  1. remove that assert completely (as was argued before, right?)
  2. make it run only on writable replicas
  3. find another way to exclude modules

please share your thoughts, and remind me what i forgot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just to be sure I understand what's happening: Is the RM_OpenKey triggered by something other than a module command, e.g. a keyspace notification or event, which is fired after some real command (SAVE) has executed? or before?

If that's the case, can we set c->cmd to NULL before the firing the event? If we do, then this code will not appear to be part of any command, which I think is better than appearing as being run as part of some read-only command.

Another question: Can this ever happen on a readonly replica? If yes, then perhaps we should set force_delete_expired to false here if we're on a read-only replica to keep it consistent with its primary, rather than only bypassing the assert.

If we do, then I guess we can drop the assert entirely.

Copy link
Member

Choose a reason for hiding this comment

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

From what i know of RedisGraph, when this code happens on a replica, should always be in a fork child, but it could still be from within a command.
i.e. when a SYNC command is received and triggers an immediate fork, the fork child process will have c->cmd still set on the stack.

What i don't like about nullifying c->cmd in the various event dispatches in module.c is that it's very far from the assertion.
i.e. we'll have to backup, nullify, and restore c->cmd and add some comments explaining that it's done to avoid an assertion on the other side of town.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree. This assert looks at things far away too...

I think we can remove the assertion and never set force_delete_expired on a writable replica:

int force_delete_expired = flags & LOOKUP_WRITE && !(server.masterhost && server.repl_slave_ro);

Copy link
Member

Choose a reason for hiding this comment

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

@zuiderkwast please make a PR.

@oranagra oranagra moved this from Done to In progress in 7.0 Feb 7, 2022
@oranagra oranagra moved this from In progress to Done in 7.0 Feb 7, 2022
oranagra pushed a commit that referenced this pull request Feb 8, 2022
There's an assertion added recently to make sure that non-write commands don't use lookupKeyWrite,
It was initially meant to be used only on read-only replicas, but we thought it'll not have enough coverage,
so used it on the masters too.
We now realize that in some cases this can cause issues for modules, so we remove the assert.

Other than that, we also make sure not to force expireIfNeeded on read-only replicas.
even if they somehow run a write command.

See #9572 (comment)
oranagra added a commit to oranagra/redis that referenced this pull request Apr 12, 2022
RM_Yield was missing a call to protectClient to prevent redis from
processing future commands of the yielding client.

Adding tests that fail without this fix.

This would be complicated to solve since nested calls to RM_Call used to
replace the current_client variable with the module temp client.

It looks like it's no longer necessary to do that, since it was added
back in redis#9890 to solve two issues, both already gone:
1. call to CONFIG SET maxmemory could trigger a module hook calling
   RM_Call. although this specific issue is gone, arguably other hooks
   like keyspace notification, can do the same.
2. an assertion in lookupKey that checks the current command of the
   current client, introduced in redis#9572 and removed in redis#10248
oranagra added a commit that referenced this pull request Apr 18, 2022
…10573)

RM_Yield was missing a call to protectClient to prevent redis from
processing future commands of the yielding client.

Adding tests that fail without this fix.

This would be complicated to solve since nested calls to RM_Call used to
replace the current_client variable with the module temp client.

It looks like it's no longer necessary to do that, since it was added
back in #9890 to solve two issues, both already gone:
1. call to CONFIG SET maxmemory could trigger a module hook calling
   RM_Call. although this specific issue is gone, arguably other hooks
   like keyspace notification, can do the same.
2. an assertion in lookupKey that checks the current command of the
   current client, introduced in #9572 and removed in #10248
enjoy-binbin added a commit to enjoy-binbin/redis that referenced this pull request Nov 27, 2022
In replica, the key expired before master's `INCR` was arrived, so INCR
creates a new key in the replica and the test failed.
```
*** [err]: Replication of an expired key does not delete the expired key in tests/integration/replication-4.tcl
Expected '0' to be equal to '1' (context: type eval line 13 cmd {assert_equal 0 [$slave exists k]} proc ::test)
```

This test is very likely to do a false positive if the `wait_for_ofs_sync`
takes longer than the expiration time, so give it a few more chances.

The test was introduced in redis#9572.
oranagra pushed a commit that referenced this pull request Nov 28, 2022
#11548)

In replica, the key expired before master's `INCR` was arrived, so INCR
creates a new key in the replica and the test failed.
```
*** [err]: Replication of an expired key does not delete the expired key in tests/integration/replication-4.tcl
Expected '0' to be equal to '1' (context: type eval line 13 cmd {assert_equal 0 [$slave exists k]} proc ::test)
```

This test is very likely to do a false positive if the `wait_for_ofs_sync`
takes longer than the expiration time, so give it a few more chances.

The test was introduced in #9572.
oranagra pushed a commit to oranagra/redis that referenced this pull request Dec 11, 2022
redis#11548)

In replica, the key expired before master's `INCR` was arrived, so INCR
creates a new key in the replica and the test failed.
```
*** [err]: Replication of an expired key does not delete the expired key in tests/integration/replication-4.tcl
Expected '0' to be equal to '1' (context: type eval line 13 cmd {assert_equal 0 [$slave exists k]} proc ::test)
```

This test is very likely to do a false positive if the `wait_for_ofs_sync`
takes longer than the expiration time, so give it a few more chances.

The test was introduced in redis#9572.

(cherry picked from commit 06b577a)
oranagra pushed a commit to oranagra/redis that referenced this pull request Dec 11, 2022
redis#11548)

In replica, the key expired before master's `INCR` was arrived, so INCR
creates a new key in the replica and the test failed.
```
*** [err]: Replication of an expired key does not delete the expired key in tests/integration/replication-4.tcl
Expected '0' to be equal to '1' (context: type eval line 13 cmd {assert_equal 0 [$slave exists k]} proc ::test)
```

This test is very likely to do a false positive if the `wait_for_ofs_sync`
takes longer than the expiration time, so give it a few more chances.

The test was introduced in redis#9572.

(cherry picked from commit 06b577a)
oranagra pushed a commit to oranagra/redis that referenced this pull request Dec 12, 2022
redis#11548)

In replica, the key expired before master's `INCR` was arrived, so INCR
creates a new key in the replica and the test failed.
```
*** [err]: Replication of an expired key does not delete the expired key in tests/integration/replication-4.tcl
Expected '0' to be equal to '1' (context: type eval line 13 cmd {assert_equal 0 [$slave exists k]} proc ::test)
```

This test is very likely to do a false positive if the `wait_for_ofs_sync`
takes longer than the expiration time, so give it a few more chances.

The test was introduced in redis#9572.

(cherry picked from commit 06b577a)
oranagra pushed a commit that referenced this pull request Dec 12, 2022
#11548)

In replica, the key expired before master's `INCR` was arrived, so INCR
creates a new key in the replica and the test failed.
```
*** [err]: Replication of an expired key does not delete the expired key in tests/integration/replication-4.tcl
Expected '0' to be equal to '1' (context: type eval line 13 cmd {assert_equal 0 [$slave exists k]} proc ::test)
```

This test is very likely to do a false positive if the `wait_for_ofs_sync`
takes longer than the expiration time, so give it a few more chances.

The test was introduced in #9572.

(cherry picked from commit 06b577a)
Mixficsol pushed a commit to Mixficsol/redis that referenced this pull request Apr 12, 2023
redis#11548)

In replica, the key expired before master's `INCR` was arrived, so INCR
creates a new key in the replica and the test failed.
```
*** [err]: Replication of an expired key does not delete the expired key in tests/integration/replication-4.tcl
Expected '0' to be equal to '1' (context: type eval line 13 cmd {assert_equal 0 [$slave exists k]} proc ::test)
```

This test is very likely to do a false positive if the `wait_for_ofs_sync`
takes longer than the expiration time, so give it a few more chances.

The test was introduced in redis#9572.
madolson pushed a commit to madolson/redis that referenced this pull request Apr 19, 2023
redis#11548)

In replica, the key expired before master's `INCR` was arrived, so INCR
creates a new key in the replica and the test failed.
```
*** [err]: Replication of an expired key does not delete the expired key in tests/integration/replication-4.tcl
Expected '0' to be equal to '1' (context: type eval line 13 cmd {assert_equal 0 [$slave exists k]} proc ::test)
```

This test is very likely to do a false positive if the `wait_for_ofs_sync`
takes longer than the expiration time, so give it a few more chances.

The test was introduced in redis#9572.
enjoy-binbin pushed a commit to enjoy-binbin/redis that referenced this pull request Jul 31, 2023
…edis#10573)

RM_Yield was missing a call to protectClient to prevent redis from
processing future commands of the yielding client.

Adding tests that fail without this fix.

This would be complicated to solve since nested calls to RM_Call used to
replace the current_client variable with the module temp client.

It looks like it's no longer necessary to do that, since it was added
back in redis#9890 to solve two issues, both already gone:
1. call to CONFIG SET maxmemory could trigger a module hook calling
   RM_Call. although this specific issue is gone, arguably other hooks
   like keyspace notification, can do the same.
2. an assertion in lookupKey that checks the current command of the
   current client, introduced in redis#9572 and removed in redis#10248
enjoy-binbin added a commit to enjoy-binbin/redis that referenced this pull request Jul 31, 2023
redis#11548)

In replica, the key expired before master's `INCR` was arrived, so INCR
creates a new key in the replica and the test failed.
```
*** [err]: Replication of an expired key does not delete the expired key in tests/integration/replication-4.tcl
Expected '0' to be equal to '1' (context: type eval line 13 cmd {assert_equal 0 [$slave exists k]} proc ::test)
```

This test is very likely to do a false positive if the `wait_for_ofs_sync`
takes longer than the expiration time, so give it a few more chances.

The test was introduced in redis#9572.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change This change can potentially break existing application release-notes indication that this issue needs to be mentioned in the release notes
Projects
Archived in project
7.0
Done
Development

Successfully merging this pull request may close these issues.

Multi-key commands about lookupKeyWrite and lookupKeyRead Writable replica may use the data of expired keys
8 participants