diff options
author | Binbin <binloveplay1314@qq.com> | 2022-10-09 13:18:34 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-09 08:18:34 +0300 |
commit | 35b3fbd90c2ad2c503c9e3d28bfbffff13099925 (patch) | |
tree | 8ca44f6572095f44f29fcddcefb8bdac3490b806 /tests/unit | |
parent | d2ad01ab3e24ca537efdd1fc6ff1ae2c657f4a51 (diff) | |
download | redis-35b3fbd90c2ad2c503c9e3d28bfbffff13099925.tar.gz |
Freeze time sampling during command execution, and scripts (#10300)
Freeze time during execution of scripts and all other commands.
This means that a key is either expired or not, and doesn't change
state during a script execution. resolves #10182
This PR try to add a new `commandTimeSnapshot` function.
The function logic is extracted from `keyIsExpired`, but the related
calls to `fixed_time_expire` and `mstime()` are removed, see below.
In commands, we will avoid calling `mstime()` multiple times
and just use the one that sampled in call. The background is,
e.g. using `PEXPIRE 1` with valgrind sometimes result in the key
being deleted rather than expired. The reason is that both `PEXPIRE`
command and `checkAlreadyExpired` call `mstime()` separately.
There are other more important changes in this PR:
1. Eliminate `fixed_time_expire`, it is no longer needed.
When we want to sample time we should always use a time snapshot.
We will use `in_nested_call` instead to update the cached time in `call`.
2. Move the call for `updateCachedTime` from `serverCron` to `afterSleep`.
Now `commandTimeSnapshot` will always return the sample time, the
`lookupKeyReadWithFlags` call in `getNodeByQuery` will get a outdated
cached time (because `processCommand` is out of the `call` context).
We put the call to `updateCachedTime` in `aftersleep`.
3. Cache the time each time the module lock Redis.
Call `updateCachedTime` in `moduleGILAfterLock`, affecting `RM_ThreadSafeContextLock`
and `RM_ThreadSafeContextTryLock`
Currently the commandTimeSnapshot change affects the following TTL commands:
- SET EX / SET PX
- EXPIRE / PEXPIRE
- SETEX / PSETEX
- GETEX EX / GETEX PX
- TTL / PTTL
- EXPIRETIME / PEXPIRETIME
- RESTORE key TTL
And other commands just use the cached mstime (including TIME).
This is considered to be a breaking change since it can break a script
that uses a loop to wait for a key to expire.
Diffstat (limited to 'tests/unit')
-rw-r--r-- | tests/unit/scripting.tcl | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/tests/unit/scripting.tcl b/tests/unit/scripting.tcl index 424017832..ca9422787 100644 --- a/tests/unit/scripting.tcl +++ b/tests/unit/scripting.tcl @@ -1376,6 +1376,118 @@ start_server {tags {"scripting needs:debug"}} { r DEBUG set-active-expire 1 } + test "TIME command using cached time" { + set res [run_script { + local result1 = {redis.call("TIME")} + redis.call("DEBUG", "SLEEP", 0.01) + local result2 = {redis.call("TIME")} + return {result1, result2} + } 0] + assert_equal [lindex $res 0] [lindex $res 1] + } + + test "Script block the time in some expiration related commands" { + # The test uses different commands to set the "same" expiration time for different keys, + # and interspersed with "DEBUG SLEEP", to verify that time is frozen in script. + # The commands involved are [P]TTL / SET EX[PX] / [P]EXPIRE / GETEX / [P]SETEX / [P]EXPIRETIME + set res [run_script { + redis.call("SET", "key1{t}", "value", "EX", 1) + redis.call("DEBUG", "SLEEP", 0.01) + + redis.call("SET", "key2{t}", "value", "PX", 1000) + redis.call("DEBUG", "SLEEP", 0.01) + + redis.call("SET", "key3{t}", "value") + redis.call("EXPIRE", "key3{t}", 1) + redis.call("DEBUG", "SLEEP", 0.01) + + redis.call("SET", "key4{t}", "value") + redis.call("PEXPIRE", "key4{t}", 1000) + redis.call("DEBUG", "SLEEP", 0.01) + + redis.call("SETEX", "key5{t}", 1, "value") + redis.call("DEBUG", "SLEEP", 0.01) + + redis.call("PSETEX", "key6{t}", 1000, "value") + redis.call("DEBUG", "SLEEP", 0.01) + + redis.call("SET", "key7{t}", "value") + redis.call("GETEX", "key7{t}", "EX", 1) + redis.call("DEBUG", "SLEEP", 0.01) + + redis.call("SET", "key8{t}", "value") + redis.call("GETEX", "key8{t}", "PX", 1000) + redis.call("DEBUG", "SLEEP", 0.01) + + local ttl_results = {redis.call("TTL", "key1{t}"), + redis.call("TTL", "key2{t}"), + redis.call("TTL", "key3{t}"), + redis.call("TTL", "key4{t}"), + redis.call("TTL", "key5{t}"), + redis.call("TTL", "key6{t}"), + redis.call("TTL", "key7{t}"), + redis.call("TTL", "key8{t}")} + + local pttl_results = {redis.call("PTTL", "key1{t}"), + redis.call("PTTL", "key2{t}"), + redis.call("PTTL", "key3{t}"), + redis.call("PTTL", "key4{t}"), + redis.call("PTTL", "key5{t}"), + redis.call("PTTL", "key6{t}"), + redis.call("PTTL", "key7{t}"), + redis.call("PTTL", "key8{t}")} + + local expiretime_results = {redis.call("EXPIRETIME", "key1{t}"), + redis.call("EXPIRETIME", "key2{t}"), + redis.call("EXPIRETIME", "key3{t}"), + redis.call("EXPIRETIME", "key4{t}"), + redis.call("EXPIRETIME", "key5{t}"), + redis.call("EXPIRETIME", "key6{t}"), + redis.call("EXPIRETIME", "key7{t}"), + redis.call("EXPIRETIME", "key8{t}")} + + local pexpiretime_results = {redis.call("PEXPIRETIME", "key1{t}"), + redis.call("PEXPIRETIME", "key2{t}"), + redis.call("PEXPIRETIME", "key3{t}"), + redis.call("PEXPIRETIME", "key4{t}"), + redis.call("PEXPIRETIME", "key5{t}"), + redis.call("PEXPIRETIME", "key6{t}"), + redis.call("PEXPIRETIME", "key7{t}"), + redis.call("PEXPIRETIME", "key8{t}")} + + return {ttl_results, pttl_results, expiretime_results, pexpiretime_results} + } 8 key1{t} key2{t} key3{t} key4{t} key5{t} key6{t} key7{t} key8{t}] + + # The elements in each list are equal. + assert_equal 1 [llength [lsort -unique [lindex $res 0]]] + assert_equal 1 [llength [lsort -unique [lindex $res 1]]] + assert_equal 1 [llength [lsort -unique [lindex $res 2]]] + assert_equal 1 [llength [lsort -unique [lindex $res 3]]] + + # Then we check that the expiration time is set successfully. + assert_morethan [lindex $res 0] 0 + assert_morethan [lindex $res 1] 0 + assert_morethan [lindex $res 2] 0 + assert_morethan [lindex $res 3] 0 + } + + test "RESTORE expired keys with expiration time" { + set res [run_script { + redis.call("SET", "key1{t}", "value") + local encoded = redis.call("DUMP", "key1{t}") + + redis.call("RESTORE", "key2{t}", 1, encoded, "REPLACE") + redis.call("DEBUG", "SLEEP", 0.01) + redis.call("RESTORE", "key3{t}", 1, encoded, "REPLACE") + + return {redis.call("PEXPIRETIME", "key2{t}"), redis.call("PEXPIRETIME", "key3{t}")} + } 3 key1{t} key2{t} key3{t}] + + # Can get the expiration time and they are all equal. + assert_morethan [lindex $res 0] 0 + assert_equal [lindex $res 0] [lindex $res 1] + } + r debug set-disable-deny-scripts 0 } } ;# foreach is_eval |