
The PR extends RM_Call with 3 new capabilities using new flags that are given to RM_Call as part of the `fmt` argument. It aims to assist modules that are getting a list of commands to be executed from the user (not hard coded as part of the module logic), think of a module that implements a new scripting language... * `S` - Run the command in a script mode, this means that it will raise an error if a command which are not allowed inside a script (flaged with the `deny-script` flag) is invoked (like SHUTDOWN). In addition, on script mode, write commands are not allowed if there is not enough good replicas (as configured with `min-replicas-to-write`) and/or a disk error happened. * `W` - no writes mode, Redis will reject any command that is marked with `write` flag. Again can be useful to modules that implement a new scripting language and wants to prevent any write commands. * `E` - Return errors as RedisModuleCallReply. Today the errors that happened before the command was invoked (like unknown commands or acl error) return a NULL reply and set errno. This might be missing important information about the failure and it is also impossible to just pass the error to the user using RM_ReplyWithCallReply. This new flag allows you to get a RedisModuleCallReply object with the relevant error message and treat it as if it was an error that was raised by the command invocation. Tests were added to verify the new code paths. In addition small refactoring was done to share some code between modules, scripts, and `processCommand` function: 1. `getAclErrorMessage` was added to `acl.c` to unified to log message extraction from the acl result 2. `checkGoodReplicasStatus` was added to `replication.c` to check the status of good replicas. It is used on `scriptVerifyWriteCommandAllow`, `RM_Call`, and `processCommand`. 3. `writeCommandsGetDiskErrorMessage` was added to `server.c` to get the error message on persistence failure. Again it is used on `scriptVerifyWriteCommandAllow`, `RM_Call`, and `processCommand`.
218 lines
7.4 KiB
Tcl
218 lines
7.4 KiB
Tcl
set testmodule [file normalize tests/modules/blockedclient.so]
|
|
|
|
start_server {tags {"modules"}} {
|
|
r module load $testmodule
|
|
|
|
test {Locked GIL acquisition} {
|
|
assert_match "OK" [r acquire_gil]
|
|
}
|
|
|
|
test {Locked GIL acquisition during multi} {
|
|
r multi
|
|
r acquire_gil
|
|
assert_equal {{Blocked client is not supported inside multi}} [r exec]
|
|
}
|
|
|
|
test {Locked GIL acquisition from RM_Call} {
|
|
assert_equal {Blocked client is not allowed} [r do_rm_call acquire_gil]
|
|
}
|
|
|
|
test {Blocking command are not block the client on RM_Call} {
|
|
r lpush l test
|
|
assert_equal [r do_rm_call blpop l 0] {l test}
|
|
|
|
r lpush l test
|
|
assert_equal [r do_rm_call brpop l 0] {l test}
|
|
|
|
r lpush l1 test
|
|
assert_equal [r do_rm_call brpoplpush l1 l2 0] {test}
|
|
assert_equal [r do_rm_call brpop l2 0] {l2 test}
|
|
|
|
r lpush l1 test
|
|
assert_equal [r do_rm_call blmove l1 l2 LEFT LEFT 0] {test}
|
|
assert_equal [r do_rm_call brpop l2 0] {l2 test}
|
|
|
|
r ZADD zset1 0 a 1 b 2 c
|
|
assert_equal [r do_rm_call bzpopmin zset1 0] {zset1 a 0}
|
|
assert_equal [r do_rm_call bzpopmax zset1 0] {zset1 c 2}
|
|
|
|
r xgroup create s g $ MKSTREAM
|
|
r xadd s * foo bar
|
|
assert {[r do_rm_call xread BLOCK 0 STREAMS s 0-0] ne {}}
|
|
assert {[r do_rm_call xreadgroup group g c BLOCK 0 STREAMS s >] ne {}}
|
|
|
|
assert {[r do_rm_call blpop empty_list 0] eq {}}
|
|
assert {[r do_rm_call brpop empty_list 0] eq {}}
|
|
assert {[r do_rm_call brpoplpush empty_list1 empty_list2 0] eq {}}
|
|
assert {[r do_rm_call blmove empty_list1 empty_list2 LEFT LEFT 0] eq {}}
|
|
|
|
assert {[r do_rm_call bzpopmin empty_zset 0] eq {}}
|
|
assert {[r do_rm_call bzpopmax empty_zset 0] eq {}}
|
|
|
|
r xgroup create empty_stream g $ MKSTREAM
|
|
assert {[r do_rm_call xread BLOCK 0 STREAMS empty_stream $] eq {}}
|
|
assert {[r do_rm_call xreadgroup group g c BLOCK 0 STREAMS empty_stream >] eq {}}
|
|
|
|
}
|
|
|
|
test {Monitor disallow inside RM_Call} {
|
|
set e {}
|
|
catch {
|
|
r do_rm_call monitor
|
|
} e
|
|
set e
|
|
} {*ERR*DENY BLOCKING*}
|
|
|
|
test {subscribe disallow inside RM_Call} {
|
|
set e {}
|
|
catch {
|
|
r do_rm_call subscribe x
|
|
} e
|
|
set e
|
|
} {*ERR*DENY BLOCKING*}
|
|
|
|
test {RM_Call from blocked client} {
|
|
r hset hash foo bar
|
|
r do_bg_rm_call hgetall hash
|
|
} {foo bar}
|
|
|
|
test {RESP version carries through to blocked client} {
|
|
for {set client_proto 2} {$client_proto <= 3} {incr client_proto} {
|
|
r hello $client_proto
|
|
r readraw 1
|
|
set ret [r do_fake_bg_true]
|
|
if {$client_proto == 2} {
|
|
assert_equal $ret {:1}
|
|
} else {
|
|
assert_equal $ret "#t"
|
|
}
|
|
r readraw 0
|
|
}
|
|
}
|
|
|
|
test {Busy module command} {
|
|
set busy_time_limit 50
|
|
set old_time_limit [lindex [r config get busy-reply-threshold] 1]
|
|
r config set busy-reply-threshold $busy_time_limit
|
|
set rd [redis_deferring_client]
|
|
|
|
# run command that blocks until released
|
|
set start [clock clicks -milliseconds]
|
|
$rd slow_fg_command 0
|
|
$rd flush
|
|
|
|
# make sure we get BUSY error, and that we didn't get it too early
|
|
assert_error {*BUSY Slow module operation*} {r ping}
|
|
assert_morethan_equal [expr [clock clicks -milliseconds]-$start] $busy_time_limit
|
|
|
|
# abort the blocking operation
|
|
r stop_slow_fg_command
|
|
wait_for_condition 50 100 {
|
|
[catch {r ping} e] == 0
|
|
} else {
|
|
fail "Failed waiting for busy command to end"
|
|
}
|
|
$rd read
|
|
|
|
#run command that blocks for 200ms
|
|
set start [clock clicks -milliseconds]
|
|
$rd slow_fg_command 200000
|
|
$rd flush
|
|
after 10 ;# try to make sure redis started running the command before we proceed
|
|
|
|
# make sure we didn't get BUSY error, it simply blocked till the command was done
|
|
r ping
|
|
assert_morethan_equal [expr [clock clicks -milliseconds]-$start] 200
|
|
$rd read
|
|
|
|
$rd close
|
|
r config set busy-reply-threshold $old_time_limit
|
|
}
|
|
|
|
test {RM_Call from blocked client} {
|
|
set busy_time_limit 50
|
|
set old_time_limit [lindex [r config get busy-reply-threshold] 1]
|
|
r config set busy-reply-threshold $busy_time_limit
|
|
|
|
# trigger slow operation
|
|
r set_slow_bg_operation 1
|
|
r hset hash foo bar
|
|
set rd [redis_deferring_client]
|
|
set start [clock clicks -milliseconds]
|
|
$rd do_bg_rm_call hgetall hash
|
|
|
|
# wait till we know we're blocked inside the module
|
|
wait_for_condition 50 100 {
|
|
[r is_in_slow_bg_operation] eq 1
|
|
} else {
|
|
fail "Failed waiting for slow operation to start"
|
|
}
|
|
|
|
# make sure we get BUSY error, and that we didn't get here too early
|
|
assert_error {*BUSY Slow module operation*} {r ping}
|
|
assert_morethan [expr [clock clicks -milliseconds]-$start] $busy_time_limit
|
|
# abort the blocking operation
|
|
r set_slow_bg_operation 0
|
|
|
|
wait_for_condition 50 100 {
|
|
[r is_in_slow_bg_operation] eq 0
|
|
} else {
|
|
fail "Failed waiting for slow operation to stop"
|
|
}
|
|
assert_equal [r ping] {PONG}
|
|
|
|
r config set busy-reply-threshold $old_time_limit
|
|
set res [$rd read]
|
|
$rd close
|
|
set _ $res
|
|
} {foo bar}
|
|
|
|
test {blocked client reaches client output buffer limit} {
|
|
r hset hash big [string repeat x 50000]
|
|
r hset hash bada [string repeat x 50000]
|
|
r hset hash boom [string repeat x 50000]
|
|
r config set client-output-buffer-limit {normal 100000 0 0}
|
|
r client setname myclient
|
|
catch {r do_bg_rm_call hgetall hash} e
|
|
assert_match "*I/O error*" $e
|
|
reconnect
|
|
set clients [r client list]
|
|
assert_no_match "*name=myclient*" $clients
|
|
}
|
|
|
|
test {module client error stats} {
|
|
r config resetstat
|
|
|
|
# simple module command that replies with string error
|
|
assert_error "ERR Unknown Redis command 'hgetalllll'." {r do_rm_call hgetalllll}
|
|
assert_equal [errorrstat ERR r] {count=1}
|
|
|
|
# module command that replies with string error from bg thread
|
|
assert_error "NULL reply returned" {r do_bg_rm_call hgetalllll}
|
|
assert_equal [errorrstat NULL r] {count=1}
|
|
|
|
# module command that returns an arity error
|
|
r do_rm_call set x x
|
|
assert_error "ERR wrong number of arguments for 'do_rm_call' command" {r do_rm_call}
|
|
assert_equal [errorrstat ERR r] {count=2}
|
|
|
|
# RM_Call that propagates an error
|
|
assert_error "WRONGTYPE*" {r do_rm_call hgetall x}
|
|
assert_equal [errorrstat WRONGTYPE r] {count=1}
|
|
assert_match {*calls=1,*,rejected_calls=0,failed_calls=1} [cmdrstat hgetall r]
|
|
|
|
# RM_Call from bg thread that propagates an error
|
|
assert_error "WRONGTYPE*" {r do_bg_rm_call hgetall x}
|
|
assert_equal [errorrstat WRONGTYPE r] {count=2}
|
|
assert_match {*calls=2,*,rejected_calls=0,failed_calls=2} [cmdrstat hgetall r]
|
|
|
|
assert_equal [s total_error_replies] 5
|
|
assert_match {*calls=4,*,rejected_calls=0,failed_calls=3} [cmdrstat do_rm_call r]
|
|
assert_match {*calls=2,*,rejected_calls=0,failed_calls=2} [cmdrstat do_bg_rm_call r]
|
|
}
|
|
|
|
test "Unload the module - blockedclient" {
|
|
assert_equal {OK} [r module unload blockedclient]
|
|
}
|
|
}
|