HFE to support AOF and replicas (#13285)
* For replica sake, rewrite commands `H*EXPIRE*` , `HSETF`, `HGETF` to have absolute unix time in msec. * On active-expiration of field, propagate HDEL to replica (`propagateHashFieldDeletion()`) * On lazy-expiration, propagate HDEL to replica (`hashTypeGetValue()` now calls `hashTypeDelete()`. It also takes care to call `propagateHashFieldDeletion()`). * Fix `H*EXPIRE*` command such that if it gets flag `LT` and it doesn’t have any expiration on the field then it will considered as valid condition. Note, replicas doesn’t make any active expiration, and should avoid lazy expiration. On `hashTypeGetValue()` it doesn't check expiration (As long as the master didn’t request to delete the field, it is valid) TODO: * Attach `dbid` to HASH metadata. See [here](https://github.com/redis/redis/pull/13209#discussion_r1593385850) --------- Co-authored-by: debing.sun <debing.sun@redis.com>
This commit is contained in:
@@ -124,7 +124,8 @@ start_server {tags {"other"}} {
|
||||
if {$::accurate} {set numops 10000} else {set numops 1000}
|
||||
test {Check consistency of different data types after a reload} {
|
||||
r flushdb
|
||||
createComplexDataset r $numops usetag
|
||||
# TODO: integrate usehexpire following next commit that will support replication
|
||||
createComplexDataset r $numops {usetag usehexpire}
|
||||
if {$::ignoredigest} {
|
||||
set _ 1
|
||||
} else {
|
||||
|
||||
@@ -64,6 +64,19 @@ proc cmp_hrandfield_result {hash_name expected_result} {
|
||||
}
|
||||
}
|
||||
|
||||
proc dumpAllHashes {client} {
|
||||
set keyAndFields(0,0) 0
|
||||
unset keyAndFields
|
||||
# keep keys sorted for comparison
|
||||
foreach key [lsort [$client keys *]] {
|
||||
set fields [$client hgetall $key]
|
||||
foreach f $fields {
|
||||
set keyAndFields($key,$f) [$client hpexpiretime $key FIELDS 1 $f]
|
||||
}
|
||||
}
|
||||
return [array get keyAndFields]
|
||||
}
|
||||
|
||||
proc hrandfieldTest {activeExpireConfig} {
|
||||
r debug set-active-expire $activeExpireConfig
|
||||
r del myhash
|
||||
@@ -155,16 +168,16 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
|
||||
test "HPEXPIRE(AT) - Test 'LT' flag ($type)" {
|
||||
r del myhash
|
||||
r hset myhash field1 value1 field2 value2
|
||||
r hset myhash field1 value1 field2 value2 field3 value3
|
||||
assert_equal [r hpexpire myhash 1000 NX FIELDS 1 field1] [list $E_OK]
|
||||
assert_equal [r hpexpire myhash 2000 NX FIELDS 1 field2] [list $E_OK]
|
||||
assert_equal [r hpexpire myhash 1500 LT FIELDS 2 field1 field2] [list $E_FAIL $E_OK]
|
||||
assert_equal [r hpexpire myhash 1500 LT FIELDS 3 field1 field2 field3] [list $E_FAIL $E_OK $E_OK]
|
||||
|
||||
r del myhash
|
||||
r hset myhash field1 value1 field2 value2
|
||||
r hset myhash field1 value1 field2 value2 field3 value3
|
||||
assert_equal [r hpexpireat myhash [expr {([clock seconds]+1000)*1000}] NX FIELDS 1 field1] [list $E_OK]
|
||||
assert_equal [r hpexpireat myhash [expr {([clock seconds]+2000)*1000}] NX FIELDS 1 field2] [list $E_OK]
|
||||
assert_equal [r hpexpireat myhash [expr {([clock seconds]+1500)*1000}] LT FIELDS 2 field1 field2] [list $E_FAIL $E_OK]
|
||||
assert_equal [r hpexpireat myhash [expr {([clock seconds]+1500)*1000}] LT FIELDS 3 field1 field2 field3] [list $E_FAIL $E_OK $E_OK]
|
||||
}
|
||||
|
||||
test "HPEXPIREAT - field not exists or TTL is in the past ($type)" {
|
||||
@@ -190,46 +203,41 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
assert_error {*invalid expire time*} {r hpexpire myhash [expr (1<<48) - [clock milliseconds] + 100 ] FIELDS 1 f1}
|
||||
}
|
||||
|
||||
test "Lazy - doesn't delete hash that all its fields got expired ($type)" {
|
||||
test "Lazy Expire - fields are lazy deleted ($type)" {
|
||||
|
||||
# TODO remove the SELECT once dbid will be embedded inside dict/listpack
|
||||
r select 0
|
||||
r debug set-active-expire 0
|
||||
r flushall
|
||||
r del myhash
|
||||
|
||||
set hash_sizes {1 15 16 17 31 32 33 40}
|
||||
foreach h $hash_sizes {
|
||||
for {set i 1} {$i <= $h} {incr i} {
|
||||
# random expiration time
|
||||
r hset hrand$h f$i v$i
|
||||
r hpexpire hrand$h [expr {50 + int(rand() * 50)}] FIELDS 1 f$i
|
||||
assert_equal 1 [r HEXISTS hrand$h f$i]
|
||||
r hset myhash f1 v1 f2 v2 f3 v3
|
||||
r hpexpire myhash 1 NX FIELDS 3 f1 f2 f3
|
||||
after 5
|
||||
|
||||
# same expiration time
|
||||
r hset same$h f$i v$i
|
||||
r hpexpire same$h 100 FIELDS 1 f$i
|
||||
assert_equal 1 [r HEXISTS same$h f$i]
|
||||
# Verify that still exists even if all fields are expired
|
||||
assert_equal 1 [r EXISTS myhash]
|
||||
|
||||
# same expiration time
|
||||
r hset mix$h f$i v$i fieldWithoutExpire$i v$i
|
||||
r hpexpire mix$h 100 FIELDS 1 f$i
|
||||
assert_equal 1 [r HEXISTS mix$h f$i]
|
||||
}
|
||||
}
|
||||
# Verify that len counts also expired fields
|
||||
assert_equal 3 [r HLEN myhash]
|
||||
|
||||
after 150
|
||||
# Trying access to expired field should delete it. Len should be updated
|
||||
assert_equal 0 [r hexists myhash f1]
|
||||
assert_equal 2 [r HLEN myhash]
|
||||
|
||||
# Trying access another expired field should delete it. Len should be updated
|
||||
assert_equal "" [r hget myhash f2]
|
||||
assert_equal 1 [r HLEN myhash]
|
||||
|
||||
# Trying access last expired field should delete it. hash shouldn't exists afterward.
|
||||
assert_equal 0 [r hstrlen myhash f3]
|
||||
assert_equal 0 [r HLEN myhash]
|
||||
assert_equal 0 [r EXISTS myhash]
|
||||
|
||||
# Verify that all fields got expired but keys wasn't lazy deleted
|
||||
foreach h $hash_sizes {
|
||||
for {set i 1} {$i <= $h} {incr i} {
|
||||
assert_equal 0 [r HEXISTS mix$h f$i]
|
||||
}
|
||||
assert_equal 1 [r EXISTS hrand$h]
|
||||
assert_equal 1 [r EXISTS same$h]
|
||||
assert_equal [expr $h * 2] [r HLEN mix$h]
|
||||
}
|
||||
# Restore default
|
||||
r debug set-active-expire 1
|
||||
}
|
||||
|
||||
test "Active - deletes hash that all its fields got expired ($type)" {
|
||||
test "Active Expire - deletes hash that all its fields got expired ($type)" {
|
||||
r flushall
|
||||
|
||||
set hash_sizes {1 15 16 17 31 32 33 40}
|
||||
@@ -375,7 +383,7 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
assert_range [r httl myhash FIELDS 1 field1] 4 5
|
||||
}
|
||||
|
||||
test "Lazy expire - delete hash with expired fields ($type)" {
|
||||
test "Lazy Expire - delete hash with expired fields ($type)" {
|
||||
r del myhash
|
||||
r debug set-active-expire 0
|
||||
r hset myhash k v
|
||||
@@ -403,7 +411,7 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
|
||||
}
|
||||
|
||||
test "Lazy expire - HLEN does count expired fields ($type)" {
|
||||
test "Lazy Expire - HLEN does count expired fields ($type)" {
|
||||
# Enforce only lazy expire
|
||||
r debug set-active-expire 0
|
||||
|
||||
@@ -432,7 +440,7 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
r debug set-active-expire 1
|
||||
}
|
||||
|
||||
test "Lazy expire - HSCAN does not report expired fields ($type)" {
|
||||
test "Lazy Expire - HSCAN does not report expired fields ($type)" {
|
||||
# Enforce only lazy expire
|
||||
r debug set-active-expire 0
|
||||
|
||||
@@ -493,7 +501,7 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
r debug set-active-expire 1
|
||||
}
|
||||
|
||||
test "Lazy expire - verify various HASH commands handling expired fields ($type)" {
|
||||
test "Lazy Expire - verify various HASH commands handling expired fields ($type)" {
|
||||
# Enforce only lazy expire
|
||||
r debug set-active-expire 0
|
||||
r del h1 h2 h3 h4 h5 h18
|
||||
@@ -676,7 +684,21 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
wait_for_condition 20 10 { [r exists myhash] == 0 } else { fail "'myhash' should be expired" }
|
||||
} {} {singledb:skip}
|
||||
|
||||
test "HPERSIST - Returns empty array if key does not exist" {
|
||||
test "HMGET - returns empty entries if fields or hash expired ($type)" {
|
||||
r debug set-active-expire 0
|
||||
r del h1 h2
|
||||
r hset h1 f1 v1 f2 v2 f3 v3
|
||||
r hset h2 f1 v1 f2 v2 f3 v3
|
||||
r hpexpire h1 10000000 NX FIELDS 1 f1
|
||||
r hpexpire h1 1 NX FIELDS 2 f2 f3
|
||||
r hpexpire h2 1 NX FIELDS 3 f1 f2 f3
|
||||
after 5
|
||||
assert_equal [r hmget h1 f1 f2 f3] {v1 {} {}}
|
||||
assert_equal [r hmget h2 f1 f2 f3] {{} {} {}}
|
||||
r debug set-active-expire 1
|
||||
}
|
||||
|
||||
test "HPERSIST - Returns empty array if key does not exist ($type)" {
|
||||
r del myhash
|
||||
# Make sure we can distinguish between an empty array and a null response
|
||||
r readraw 1
|
||||
@@ -743,6 +765,53 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
assert_equal [lsort [r hgetall myhash]] "1 2 3 a b c"
|
||||
assert_equal [r hexpiretime myhash FIELDS 3 a b c] {-1 -1 -1}
|
||||
}
|
||||
|
||||
test {HINCRBY - discards pending expired field and reset its value} {
|
||||
r debug set-active-expire 0
|
||||
r del h1 h2
|
||||
r hset h1 f1 10 f2 2
|
||||
r hset h2 f1 10
|
||||
assert_equal [r HINCRBY h1 f1 2] 12
|
||||
assert_equal [r HINCRBY h2 f1 2] 12
|
||||
r HPEXPIRE h1 10 FIELDS 1 f1
|
||||
r HPEXPIRE h2 10 FIELDS 1 f1
|
||||
after 15
|
||||
assert_equal [r HINCRBY h1 f1 1] 1
|
||||
assert_equal [r HINCRBY h2 f1 1] 1
|
||||
r debug set-active-expire 1
|
||||
}
|
||||
|
||||
test {HINCRBY - preserve expiration time of the field} {
|
||||
r del h1
|
||||
r hset h1 f1 10
|
||||
r hpexpire h1 20 FIELDS 1 f1
|
||||
assert_equal [r HINCRBY h1 f1 2] 12
|
||||
assert_range [r HPTTL h1 FIELDS 1 f1] 1 20
|
||||
}
|
||||
|
||||
|
||||
test {HINCRBYFLOAT - discards pending expired field and reset its value} {
|
||||
r debug set-active-expire 0
|
||||
r del h1 h2
|
||||
r hset h1 f1 10 f2 2
|
||||
r hset h2 f1 10
|
||||
assert_equal [r HINCRBYFLOAT h1 f1 2] 12
|
||||
assert_equal [r HINCRBYFLOAT h2 f1 2] 12
|
||||
r HPEXPIRE h1 10 FIELDS 1 f1
|
||||
r HPEXPIRE h2 10 FIELDS 1 f1
|
||||
after 15
|
||||
assert_equal [r HINCRBYFLOAT h1 f1 1] 1
|
||||
assert_equal [r HINCRBYFLOAT h2 f1 1] 1
|
||||
r debug set-active-expire 1
|
||||
}
|
||||
|
||||
test {HINCRBYFLOAT - preserve expiration time of the field} {
|
||||
r del h1
|
||||
r hset h1 f1 10
|
||||
r hpexpire h1 20 FIELDS 1 f1
|
||||
assert_equal [r HINCRBYFLOAT h1 f1 2.5] 12.5
|
||||
assert_range [r HPTTL h1 FIELDS 1 f1] 1 20
|
||||
}
|
||||
}
|
||||
|
||||
r config set hash-max-listpack-entries 512
|
||||
@@ -878,3 +947,205 @@ start_server {tags {"external:skip needs:debug"}} {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"external:skip needs:debug"}} {
|
||||
foreach type {listpack ht} {
|
||||
if {$type eq "ht"} {
|
||||
r config set hash-max-listpack-entries 0
|
||||
} else {
|
||||
r config set hash-max-listpack-entries 512
|
||||
}
|
||||
|
||||
test "Command rewrite and expired hash fields are propagated to replica ($type)" {
|
||||
start_server {overrides {appendonly {yes} appendfsync always} tags {external:skip}} {
|
||||
|
||||
set aof [get_last_incr_aof_path r]
|
||||
r hset h1 f1 v1 f2 v2
|
||||
|
||||
r hpexpire h1 20 FIELDS 1 f1
|
||||
r hpexpire h1 30 FIELDS 1 f2
|
||||
r hpexpire h1 30 FIELDS 1 non_exists_field
|
||||
r hset h2 f1 v1 f2 v2 f3 v3 f4 v4
|
||||
r hpexpire h2 40 FIELDS 2 f1 non_exists_field
|
||||
r hpexpire h2 50 FIELDS 1 f2
|
||||
r hpexpireat h2 [expr [clock seconds]*1000+100000] LT FIELDS 1 f3
|
||||
r hexpireat h2 [expr [clock seconds]+10] NX FIELDS 1 f4
|
||||
|
||||
wait_for_condition 50 100 {
|
||||
[r hlen h2] eq 2
|
||||
} else {
|
||||
fail "Field f2 of hash h2 wasn't deleted"
|
||||
}
|
||||
|
||||
# Assert that each TTL-related command are persisted with absolute timestamps in AOF
|
||||
assert_aof_content $aof {
|
||||
{select *}
|
||||
{hset h1 f1 v1 f2 v2}
|
||||
{hpexpireat h1 * FIELDS 1 f1}
|
||||
{hpexpireat h1 * FIELDS 1 f2}
|
||||
{hset h2 f1 v1 f2 v2 f3 v3 f4 v4}
|
||||
{hpexpireat h2 * FIELDS 2 f1 non_exists_field}
|
||||
{hpexpireat h2 * FIELDS 1 f2}
|
||||
{hpexpireat h2 * FIELDS 1 f3}
|
||||
{hpexpireat h2 * FIELDS 1 f4}
|
||||
{hdel h1 f1}
|
||||
{hdel h1 f2}
|
||||
{hdel h2 f1}
|
||||
{hdel h2 f2}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "Lazy Expire - fields are lazy deleted and propagated to replicas ($type)" {
|
||||
start_server {overrides {appendonly {yes} appendfsync always} tags {external:skip}} {
|
||||
r debug set-active-expire 0
|
||||
set aof [get_last_incr_aof_path r]
|
||||
|
||||
r del myhash
|
||||
|
||||
r hset myhash f1 v1 f2 v2 f3 v3
|
||||
r hpexpire myhash 1 NX FIELDS 3 f1 f2 f3
|
||||
after 5
|
||||
|
||||
# Verify that still exists even if all fields are expired
|
||||
assert_equal 1 [r EXISTS myhash]
|
||||
|
||||
# Verify that len counts also expired fields
|
||||
assert_equal 3 [r HLEN myhash]
|
||||
|
||||
# Trying access to expired field should delete it. Len should be updated
|
||||
assert_equal 0 [r hexists myhash f1]
|
||||
assert_equal 2 [r HLEN myhash]
|
||||
|
||||
# Trying access another expired field should delete it. Len should be updated
|
||||
assert_equal "" [r hget myhash f2]
|
||||
assert_equal 1 [r HLEN myhash]
|
||||
|
||||
# Trying access last expired field should delete it. hash shouldn't exists afterward.
|
||||
assert_equal 0 [r hstrlen myhash f3]
|
||||
assert_equal 0 [r HLEN myhash]
|
||||
assert_equal 0 [r EXISTS myhash]
|
||||
|
||||
wait_for_condition 50 100 { [r exists h1] == 0 } else { fail "hash h1 wasn't deleted" }
|
||||
|
||||
# HDEL are propagated as expected
|
||||
assert_aof_content $aof {
|
||||
{select *}
|
||||
{hset myhash f1 v1 f2 v2 f3 v3}
|
||||
{hpexpireat myhash * NX FIELDS 3 f1 f2 f3}
|
||||
{hdel myhash f1}
|
||||
{hdel myhash f2}
|
||||
{hdel myhash f3}
|
||||
}
|
||||
r debug set-active-expire 1
|
||||
}
|
||||
}
|
||||
|
||||
# Start a new server with empty data and AOF file.
|
||||
start_server {overrides {appendonly {yes} appendfsync always} tags {external:skip}} {
|
||||
|
||||
# Based on test at expire.tcl: " All time-to-live(TTL) in commands are propagated as absolute ..."
|
||||
test {All TTLs in commands are propagated as absolute timestamp in milliseconds in AOF} {
|
||||
|
||||
set aof [get_last_incr_aof_path r]
|
||||
|
||||
r hset h1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6
|
||||
r hexpireat h1 [expr [clock seconds]+100] NX FIELDS 1 f1
|
||||
r hpexpireat h1 [expr [clock seconds]*1000+100000] NX FIELDS 1 f2
|
||||
r hpexpire h1 100000 NX FIELDS 3 f3 f4 f5
|
||||
r hexpire h1 100000 FIELDS 1 f6
|
||||
r hset h5 f1 v1
|
||||
|
||||
assert_aof_content $aof {
|
||||
{select *}
|
||||
{hset h1 f1 v1 f2 v2 f3 v3 f4 v4 f5 v5 f6 v6}
|
||||
{hpexpireat h1 * FIELDS 1 f1}
|
||||
{hpexpireat h1 * FIELDS 1 f2}
|
||||
{hpexpireat h1 * NX FIELDS 3 f3 f4 f5}
|
||||
{hpexpireat h1 * FIELDS 1 f6}
|
||||
{hset h5 f1 v1}
|
||||
}
|
||||
|
||||
array set keyAndFields1 [dumpAllHashes r]
|
||||
# Let some time pass and reload data from AOF
|
||||
after 2000
|
||||
r debug loadaof
|
||||
array set keyAndFields2 [dumpAllHashes r]
|
||||
|
||||
# Assert that absolute TTLs are the same
|
||||
assert_equal [array get keyAndFields1] [array get keyAndFields2]
|
||||
|
||||
} {} {needs:debug}
|
||||
}
|
||||
|
||||
# Based on test, with same name, at expire.tcl:
|
||||
test {All TTL in commands are propagated as absolute timestamp in replication stream} {
|
||||
# Make sure that both relative and absolute expire commands are propagated
|
||||
# Consider also comment of the test, with same name, at expire.tcl
|
||||
|
||||
r flushall ; # Clean up keyspace to avoid interference by keys from other tests
|
||||
set repl [attach_to_replication_stream]
|
||||
|
||||
r hset h1 f1 v1
|
||||
r hexpireat h1 [expr [clock seconds]+100] NX FIELDS 1 f1
|
||||
r hset h2 f2 v2
|
||||
r hpexpireat h2 [expr [clock seconds]*1000+100000] NX FIELDS 1 f2
|
||||
r hset h3 f3 v3 f4 v4
|
||||
r hexpire h3 100 FIELDS 3 f3 f4 non_exists_field
|
||||
|
||||
assert_replication_stream $repl {
|
||||
{select *}
|
||||
{hset h1 f1 v1}
|
||||
{hpexpireat h1 * NX FIELDS 1 f1}
|
||||
{hset h2 f2 v2}
|
||||
{hpexpireat h2 * NX FIELDS 1 f2}
|
||||
{hset h3 f3 v3 f4 v4}
|
||||
{hpexpireat h3 * FIELDS 3 f3 f4 non_exists_field}
|
||||
}
|
||||
close_replication_stream $repl
|
||||
} {} {needs:repl}
|
||||
|
||||
# Start another server to test replication of TTLs
|
||||
start_server {tags {needs:repl external:skip}} {
|
||||
# Set the outer layer server as primary
|
||||
set primary [srv -1 client]
|
||||
set primary_host [srv -1 host]
|
||||
set primary_port [srv -1 port]
|
||||
# Set this inner layer server as replica
|
||||
set replica [srv 0 client]
|
||||
|
||||
# Server should have role slave
|
||||
$replica replicaof $primary_host $primary_port
|
||||
wait_for_condition 50 100 {
|
||||
[s 0 role] eq {slave}
|
||||
} else {
|
||||
fail "Replication not started."
|
||||
}
|
||||
|
||||
# Based on test, with same name, at expire.tcl
|
||||
test {For all replicated TTL-related commands, absolute expire times are identical on primary and replica} {
|
||||
# Apply each TTL-related command to a unique key on primary
|
||||
$primary flushall
|
||||
$primary hset h1 f v
|
||||
$primary hexpireat h1 [expr [clock seconds]+10000] FIELDS 1 f
|
||||
$primary hset h2 f v
|
||||
$primary hpexpireat h2 [expr [clock milliseconds]+100000] FIELDS 1 f
|
||||
$primary hset h3 f v
|
||||
$primary hexpire h3 100 NX FIELDS 1 f
|
||||
$primary hset h4 f v
|
||||
$primary hpexpire h4 100000 NX FIELDS 1 f
|
||||
$primary hset h5 f v
|
||||
$primary hpexpireat h5 [expr [clock milliseconds]-100000] FIELDS 1 f
|
||||
$primary hset h9 f v
|
||||
|
||||
# Wait for replica to get the keys and TTLs
|
||||
assert {[$primary wait 1 0] == 1}
|
||||
|
||||
# Verify absolute TTLs are identical on primary and replica for all keys
|
||||
# This is because TTLs are always replicated as absolute values
|
||||
assert_equal [dumpAllHashes $primary] [dumpAllHashes $replica]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user