Unverified Commit 33fc0fbf authored by Moti Cohen's avatar Moti Cohen Committed by GitHub
Browse files

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: default avatardebing.sun <debing.sun@redis.com>
parent 6a11d458
......@@ -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]
}
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment