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
Conversation
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'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.
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.
86dd019
to
6f01fb0
Compare
0c09d59
to
32a7045
Compare
@zuiderkwast i think you should avoid doing |
@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. |
there is a way to check if the key exists without deleting it (DEBUG OBJECT). |
} | ||
} | ||
|
||
test {Replication of an expired key does not delete the expired key} { |
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.
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 ofwait_for_ofs_sync
exceeded one second. (or maybe related the KILL).
I didn't think of a good way:
- maybe we can use $slave debug sleep ? (But there are also such problems)
- add a retry
Check that k is locigally expired but is present in the replica.
also there was a typo (locigally)
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.
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?
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.
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
…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))); |
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.
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
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.
@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:
- remove that assert completely (as was argued before, right?)
- make it run only on writable replicas
- find another way to exclude modules
please share your thoughts, and remind me what i forgot.
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.
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.
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.
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.
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.
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);
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.
@zuiderkwast please make a PR.
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)
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
…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
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.
#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.
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)
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)
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)
#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)
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.
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.
…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
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.
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.