redict/tests/unit/moduleapi/testrdb.tcl
Meir Shpilraien (Spielrein) b43f254813
Avoid saving module aux on RDB if no aux data was saved by the module. (#11374)
### Background

The issue is that when saving an RDB with module AUX data, the module AUX metadata
(moduleid, when, ...) is saved to the RDB even though the module did not saved any actual data.
This prevent loading the RDB in the absence of the module (although there is no actual data in
the RDB that requires the module to be loaded).

### Solution

The solution suggested in this PR is that module AUX will be saved on the RDB only if the module
actually saved something during `aux_save` function.

To support backward compatibility, we introduce `aux_save2` callback that acts the same as
`aux_save` with the tiny change of avoid saving the aux field if no data was actually saved by
the module. Modules can use the new API to make sure that if they have no data to save,
then it will be possible to load the created RDB even without the module.

### Concerns

A module may register for the aux load and save hooks just in order to be notified when
saving or loading starts or completed (there are better ways to do that, but it still possible
that someone used it).

However, if a module didn't save a single field in the save callback, it means it's not allowed
to read in the read callback, since it has no way to distinguish between empty and non-empty
payloads. furthermore, it means that if the module did that, it must never change it, since it'll
break compatibility with it's old RDB files, so this is really not a valid use case.

Since some modules (ones who currently save one field indicating an empty payload), need
to know if saving an empty payload is valid, and if Redis is gonna ignore an empty payload
or store it, we opted to add a new API (rather than change behavior of an existing API and
expect modules to check the redis version)

### Technical Details

To avoid saving AUX data on RDB, we change the code to first save the AUX metadata
(moduleid, when, ...) into a temporary buffer. The buffer is then flushed to the rio at the first
time the module makes a write operation inside the `aux_save` function. If the module saves
nothing (and `aux_save2` was used), the entire temporary buffer is simply dropped and no
data about this AUX field is saved to the RDB. This make it possible to load the RDB even in
the absence of the module.

Test was added to verify the fix.
2022-10-18 19:45:46 +03:00

307 lines
15 KiB
Tcl

# This module can be configure with multiple options given as flags on module load time
# 0 - not aux fields will be declared (this is the default)
# 1 << 0 - use aux_save2 api
# 1 << 1 - call aux callback before key space
# 1 << 2 - call aux callback after key space
# 1 << 3 - do not save data on aux callback
set testmodule [file normalize tests/modules/testrdb.so]
tags "modules" {
test {modules are able to persist types} {
start_server [list overrides [list loadmodule "$testmodule"]] {
r testrdb.set.key key1 value1
assert_equal "value1" [r testrdb.get.key key1]
r debug reload
assert_equal "value1" [r testrdb.get.key key1]
}
}
test {modules global are lost without aux} {
set server_path [tmpdir "server.module-testrdb"]
start_server [list overrides [list loadmodule "$testmodule" "dir" $server_path] keep_persistence true] {
r testrdb.set.before global1
assert_equal "global1" [r testrdb.get.before]
}
start_server [list overrides [list loadmodule "$testmodule" "dir" $server_path]] {
assert_equal "" [r testrdb.get.before]
}
}
test {aux that saves no data are not saved to the rdb when aux_save2 is used} {
set server_path [tmpdir "server.module-testrdb"]
puts $server_path
# 15 == 1111 - use aux_save2 before and after key space without data
start_server [list overrides [list loadmodule "$testmodule 15" "dir" $server_path] keep_persistence true] {
r set x 1
r save
}
start_server [list overrides [list "dir" $server_path] keep_persistence true] {
# make sure server started successfully without the module.
assert_equal {1} [r get x]
}
}
test {aux that saves no data are saved to the rdb when aux_save is used} {
set server_path [tmpdir "server.module-testrdb"]
puts $server_path
# 14 == 1110 - use aux_save before and after key space without data
start_server [list overrides [list loadmodule "$testmodule 14" "dir" $server_path] keep_persistence true] {
r set x 1
r save
}
start_server [list overrides [list loadmodule "$testmodule 14" "dir" $server_path] keep_persistence true] {
# make sure server started successfully and aux_save was called twice.
assert_equal {1} [r get x]
assert_equal {2} [r testrdb.get.n_aux_load_called]
}
}
foreach test_case {6 7} {
# 6 == 0110 - use aux_save before and after key space with data
# 7 == 0111 - use aux_save2 before and after key space with data
test {modules are able to persist globals before and after} {
set server_path [tmpdir "server.module-testrdb"]
start_server [list overrides [list loadmodule "$testmodule $test_case" "dir" $server_path] keep_persistence true] {
r testrdb.set.before global1
r testrdb.set.after global2
assert_equal "global1" [r testrdb.get.before]
assert_equal "global2" [r testrdb.get.after]
}
start_server [list overrides [list loadmodule "$testmodule $test_case" "dir" $server_path]] {
assert_equal "global1" [r testrdb.get.before]
assert_equal "global2" [r testrdb.get.after]
}
}
}
foreach test_case {4 5} {
# 4 == 0100 - use aux_save after key space with data
# 5 == 0101 - use aux_save2 after key space with data
test {modules are able to persist globals just after} {
set server_path [tmpdir "server.module-testrdb"]
start_server [list overrides [list loadmodule "$testmodule $test_case" "dir" $server_path] keep_persistence true] {
r testrdb.set.after global2
assert_equal "global2" [r testrdb.get.after]
}
start_server [list overrides [list loadmodule "$testmodule $test_case" "dir" $server_path]] {
assert_equal "global2" [r testrdb.get.after]
}
}
}
test {Verify module options info} {
start_server [list overrides [list loadmodule "$testmodule"]] {
assert_match "*\[handle-io-errors|handle-repl-async-load\]*" [r info modules]
}
}
tags {repl} {
test {diskless loading short read with module} {
start_server [list overrides [list loadmodule "$testmodule"]] {
set replica [srv 0 client]
set replica_host [srv 0 host]
set replica_port [srv 0 port]
start_server [list overrides [list loadmodule "$testmodule"]] {
set master [srv 0 client]
set master_host [srv 0 host]
set master_port [srv 0 port]
# Set master and replica to use diskless replication
$master config set repl-diskless-sync yes
$master config set rdbcompression no
$replica config set repl-diskless-load swapdb
$master config set hz 500
$replica config set hz 500
$master config set dynamic-hz no
$replica config set dynamic-hz no
set start [clock clicks -milliseconds]
for {set k 0} {$k < 30} {incr k} {
r testrdb.set.key key$k [string repeat A [expr {int(rand()*1000000)}]]
}
if {$::verbose} {
set end [clock clicks -milliseconds]
set duration [expr $end - $start]
puts "filling took $duration ms (TODO: use pipeline)"
set start [clock clicks -milliseconds]
}
# Start the replication process...
set loglines [count_log_lines -1]
$master config set repl-diskless-sync-delay 0
$replica replicaof $master_host $master_port
# kill the replication at various points
set attempts 100
if {$::accurate} { set attempts 500 }
for {set i 0} {$i < $attempts} {incr i} {
# wait for the replica to start reading the rdb
# using the log file since the replica only responds to INFO once in 2mb
set res [wait_for_log_messages -1 {"*Loading DB in memory*"} $loglines 2000 1]
set loglines [lindex $res 1]
# add some additional random sleep so that we kill the master on a different place each time
after [expr {int(rand()*50)}]
# kill the replica connection on the master
set killed [$master client kill type replica]
set res [wait_for_log_messages -1 {"*Internal error in RDB*" "*Finished with success*" "*Successful partial resynchronization*"} $loglines 500 10]
if {$::verbose} { puts $res }
set log_text [lindex $res 0]
set loglines [lindex $res 1]
if {![string match "*Internal error in RDB*" $log_text]} {
# force the replica to try another full sync
$master multi
$master client kill type replica
$master set asdf asdf
# fill replication backlog with new content
$master config set repl-backlog-size 16384
for {set keyid 0} {$keyid < 10} {incr keyid} {
$master set "$keyid string_$keyid" [string repeat A 16384]
}
$master exec
}
# wait for loading to stop (fail)
# After a loading successfully, next loop will enter `async_loading`
wait_for_condition 1000 1 {
[s -1 async_loading] eq 0 &&
[s -1 loading] eq 0
} else {
fail "Replica didn't disconnect"
}
}
if {$::verbose} {
set end [clock clicks -milliseconds]
set duration [expr $end - $start]
puts "test took $duration ms"
}
# enable fast shutdown
$master config set rdb-key-save-delay 0
}
}
}
# Module events for diskless load swapdb when async_loading (matching master replid)
foreach test_case {6 7} {
# 6 == 0110 - use aux_save before and after key space with data
# 7 == 0111 - use aux_save2 before and after key space with data
foreach testType {Successful Aborted} {
start_server [list overrides [list loadmodule "$testmodule $test_case"] tags [list external:skip]] {
set replica [srv 0 client]
set replica_host [srv 0 host]
set replica_port [srv 0 port]
set replica_log [srv 0 stdout]
start_server [list overrides [list loadmodule "$testmodule $test_case"]] {
set master [srv 0 client]
set master_host [srv 0 host]
set master_port [srv 0 port]
set start [clock clicks -milliseconds]
# Set master and replica to use diskless replication on swapdb mode
$master config set repl-diskless-sync yes
$master config set repl-diskless-sync-delay 0
$master config set save ""
$replica config set repl-diskless-load swapdb
$replica config set save ""
# Initial sync to have matching replids between master and replica
$replica replicaof $master_host $master_port
# Let replica finish initial sync with master
wait_for_condition 100 100 {
[s -1 master_link_status] eq "up"
} else {
fail "Master <-> Replica didn't finish sync"
}
# Set global values on module so we can check if module event callbacks will pick it up correctly
$master testrdb.set.before value1_master
$replica testrdb.set.before value1_replica
# Put different data sets on the master and replica
# We need to put large keys on the master since the replica replies to info only once in 2mb
$replica debug populate 200 slave 10
$master debug populate 1000 master 100000
$master config set rdbcompression no
# Force the replica to try another full sync (this time it will have matching master replid)
$master multi
$master client kill type replica
# Fill replication backlog with new content
$master config set repl-backlog-size 16384
for {set keyid 0} {$keyid < 10} {incr keyid} {
$master set "$keyid string_$keyid" [string repeat A 16384]
}
$master exec
switch $testType {
"Aborted" {
# Set master with a slow rdb generation, so that we can easily intercept loading
# 10ms per key, with 1000 keys is 10 seconds
$master config set rdb-key-save-delay 10000
test {Diskless load swapdb RedisModuleEvent_ReplAsyncLoad handling: during loading, can keep module variable same as before} {
# Wait for the replica to start reading the rdb and module for acknowledgement
# We wanna abort only after the temp db was populated by REDISMODULE_AUX_BEFORE_RDB
wait_for_condition 100 100 {
[s -1 async_loading] eq 1 && [$replica testrdb.async_loading.get.before] eq "value1_master"
} else {
fail "Module didn't receive or react to REDISMODULE_SUBEVENT_REPL_ASYNC_LOAD_STARTED"
}
assert_equal [$replica dbsize] 200
assert_equal value1_replica [$replica testrdb.get.before]
}
# Make sure that next sync will not start immediately so that we can catch the replica in between syncs
$master config set repl-diskless-sync-delay 5
# Kill the replica connection on the master
set killed [$master client kill type replica]
test {Diskless load swapdb RedisModuleEvent_ReplAsyncLoad handling: when loading aborted, can keep module variable same as before} {
# Wait for loading to stop (fail) and module for acknowledgement
wait_for_condition 100 100 {
[s -1 async_loading] eq 0 && [$replica testrdb.async_loading.get.before] eq ""
} else {
fail "Module didn't receive or react to REDISMODULE_SUBEVENT_REPL_ASYNC_LOAD_ABORTED"
}
assert_equal [$replica dbsize] 200
assert_equal value1_replica [$replica testrdb.get.before]
}
# Speed up shutdown
$master config set rdb-key-save-delay 0
}
"Successful" {
# Let replica finish sync with master
wait_for_condition 100 100 {
[s -1 master_link_status] eq "up"
} else {
fail "Master <-> Replica didn't finish sync"
}
test {Diskless load swapdb RedisModuleEvent_ReplAsyncLoad handling: after db loaded, can set module variable with new value} {
assert_equal [$replica dbsize] 1010
assert_equal value1_master [$replica testrdb.get.before]
}
}
}
if {$::verbose} {
set end [clock clicks -milliseconds]
set duration [expr $end - $start]
puts "test took $duration ms"
}
}
}
}
}
}
}