redict/tests/integration/redis-cli.tcl

481 lines
15 KiB
Tcl
Raw Normal View History

source tests/support/cli.tcl
Improve test suite to handle external servers better. (#9033) This commit revives the improves the ability to run the test suite against external servers, instead of launching and managing `redis-server` processes as part of the test fixture. This capability existed in the past, using the `--host` and `--port` options. However, it was quite limited and mostly useful when running a specific tests. Attempting to run larger chunks of the test suite experienced many issues: * Many tests depend on being able to start and control `redis-server` themselves, and there's no clear distinction between external server compatible and other tests. * Cluster mode is not supported (resulting with `CROSSSLOT` errors). This PR cleans up many things and makes it possible to run the entire test suite against an external server. It also provides more fine grained controls to handle cases where the external server supports a subset of the Redis commands, limited number of databases, cluster mode, etc. The tests directory now contains a `README.md` file that describes how this works. This commit also includes additional cleanups and fixes: * Tests can now be tagged. * Tag-based selection is now unified across `start_server`, `tags` and `test`. * More information is provided about skipped or ignored tests. * Repeated patterns in tests have been extracted to common procedures, both at a global level and on a per-test file basis. * Cleaned up some cases where test setup was based on a previous test executing (a major anti-pattern that repeats itself in many places). * Cleaned up some cases where test teardown was not part of a test (in the future we should have dedicated teardown code that executes even when tests fail). * Fixed some tests that were flaky running on external servers.
2021-06-09 08:13:24 -04:00
if {$::singledb} {
set ::dbnum 0
} else {
set ::dbnum 9
}
start_server {tags {"cli"}} {
Improve test suite to handle external servers better. (#9033) This commit revives the improves the ability to run the test suite against external servers, instead of launching and managing `redis-server` processes as part of the test fixture. This capability existed in the past, using the `--host` and `--port` options. However, it was quite limited and mostly useful when running a specific tests. Attempting to run larger chunks of the test suite experienced many issues: * Many tests depend on being able to start and control `redis-server` themselves, and there's no clear distinction between external server compatible and other tests. * Cluster mode is not supported (resulting with `CROSSSLOT` errors). This PR cleans up many things and makes it possible to run the entire test suite against an external server. It also provides more fine grained controls to handle cases where the external server supports a subset of the Redis commands, limited number of databases, cluster mode, etc. The tests directory now contains a `README.md` file that describes how this works. This commit also includes additional cleanups and fixes: * Tests can now be tagged. * Tag-based selection is now unified across `start_server`, `tags` and `test`. * More information is provided about skipped or ignored tests. * Repeated patterns in tests have been extracted to common procedures, both at a global level and on a per-test file basis. * Cleaned up some cases where test setup was based on a previous test executing (a major anti-pattern that repeats itself in many places). * Cleaned up some cases where test teardown was not part of a test (in the future we should have dedicated teardown code that executes even when tests fail). * Fixed some tests that were flaky running on external servers.
2021-06-09 08:13:24 -04:00
proc open_cli {{opts ""} {infile ""}} {
if { $opts == "" } {
set opts "-n $::dbnum"
}
set ::env(TERM) dumb
set cmdline [rediscli [srv host] [srv port] $opts]
if {$infile ne ""} {
set cmdline "$cmdline < $infile"
set mode "r"
} else {
set mode "r+"
}
set fd [open "|$cmdline" $mode]
fconfigure $fd -buffering none
fconfigure $fd -blocking false
fconfigure $fd -translation binary
set _ $fd
}
proc close_cli {fd} {
close $fd
}
proc read_cli {fd} {
set ret [read $fd]
while {[string length $ret] == 0} {
after 10
set ret [read $fd]
}
# We may have a short read, try to read some more.
set empty_reads 0
while {$empty_reads < 5} {
set buf [read $fd]
if {[string length $buf] == 0} {
after 10
incr empty_reads
} else {
append ret $buf
set empty_reads 0
}
}
return $ret
}
proc write_cli {fd buf} {
puts $fd $buf
flush $fd
}
2010-08-25 08:08:32 -04:00
# Helpers to run tests in interactive mode
proc format_output {output} {
set _ [string trimright [regsub -all "\r" $output ""] "\n"]
}
proc run_command {fd cmd} {
write_cli $fd $cmd
set _ [format_output [read_cli $fd]]
}
proc test_interactive_cli {name code} {
set ::env(FAKETTY) 1
set fd [open_cli]
test "Interactive CLI: $name" $code
close_cli $fd
unset ::env(FAKETTY)
}
2010-08-25 08:08:32 -04:00
# Helpers to run tests where stdout is not a tty
proc write_tmpfile {contents} {
set tmp [tmpfile "cli"]
set tmpfd [open $tmp "w"]
puts -nonewline $tmpfd $contents
close $tmpfd
set _ $tmp
}
proc _run_cli {host port db opts args} {
set cmd [rediscli $host $port [list -n $db {*}$args]]
foreach {key value} $opts {
if {$key eq "pipe"} {
set cmd "sh -c \"$value | $cmd\""
}
if {$key eq "path"} {
set cmd "$cmd < $value"
}
}
set fd [open "|$cmd" "r"]
fconfigure $fd -buffering none
fconfigure $fd -translation binary
set resp [read $fd 1048576]
close $fd
set _ [format_output $resp]
}
proc run_cli {args} {
_run_cli [srv host] [srv port] $::dbnum {} {*}$args
}
proc run_cli_with_input_pipe {mode cmd args} {
if {$mode == "x" } {
_run_cli [srv host] [srv port] $::dbnum [list pipe $cmd] -x {*}$args
} elseif {$mode == "X"} {
_run_cli [srv host] [srv port] $::dbnum [list pipe $cmd] -X tag {*}$args
}
}
proc run_cli_with_input_file {mode path args} {
if {$mode == "x" } {
_run_cli [srv host] [srv port] $::dbnum [list path $path] -x {*}$args
} elseif {$mode == "X"} {
_run_cli [srv host] [srv port] $::dbnum [list path $path] -X tag {*}$args
}
}
proc run_cli_host_port_db {host port db args} {
_run_cli $host $port $db {} {*}$args
}
proc test_nontty_cli {name code} {
test "Non-interactive non-TTY CLI: $name" $code
}
2010-08-25 08:15:41 -04:00
# Helpers to run tests where stdout is a tty (fake it)
proc test_tty_cli {name code} {
2010-08-25 08:15:41 -04:00
set ::env(FAKETTY) 1
test "Non-interactive TTY CLI: $name" $code
2010-08-25 08:15:41 -04:00
unset ::env(FAKETTY)
}
test_interactive_cli "INFO response should be printed raw" {
set lines [split [run_command $fd info] "\n"]
foreach line $lines {
if {![regexp {^$|^#|^[^#:]+:} $line]} {
fail "Malformed info line: $line"
}
}
}
test_interactive_cli "Status reply" {
assert_equal "OK" [run_command $fd "set key foo"]
}
test_interactive_cli "Integer reply" {
assert_equal "(integer) 1" [run_command $fd "incr counter"]
}
test_interactive_cli "Bulk reply" {
r set key foo
assert_equal "\"foo\"" [run_command $fd "get key"]
}
test_interactive_cli "Multi-bulk reply" {
r rpush list foo
r rpush list bar
assert_equal "1) \"foo\"\n2) \"bar\"" [run_command $fd "lrange list 0 -1"]
}
test_interactive_cli "Parsing quotes" {
assert_equal "OK" [run_command $fd "set key \"bar\""]
assert_equal "bar" [r get key]
assert_equal "OK" [run_command $fd "set key \" bar \""]
assert_equal " bar " [r get key]
assert_equal "OK" [run_command $fd "set key \"\\\"bar\\\"\""]
assert_equal "\"bar\"" [r get key]
assert_equal "OK" [run_command $fd "set key \"\tbar\t\""]
assert_equal "\tbar\t" [r get key]
# invalid quotation
assert_equal "Invalid argument(s)" [run_command $fd "get \"\"key"]
assert_equal "Invalid argument(s)" [run_command $fd "get \"key\"x"]
# quotes after the argument are weird, but should be allowed
assert_equal "OK" [run_command $fd "set key\"\" bar"]
assert_equal "bar" [r get key]
}
test_tty_cli "Status reply" {
assert_equal "OK" [run_cli set key bar]
assert_equal "bar" [r get key]
}
test_tty_cli "Integer reply" {
r del counter
assert_equal "(integer) 1" [run_cli incr counter]
}
test_tty_cli "Bulk reply" {
r set key "tab\tnewline\n"
assert_equal "\"tab\\tnewline\\n\"" [run_cli get key]
}
test_tty_cli "Multi-bulk reply" {
r del list
r rpush list foo
r rpush list bar
assert_equal "1) \"foo\"\n2) \"bar\"" [run_cli lrange list 0 -1]
}
test_tty_cli "Read last argument from pipe" {
assert_equal "OK" [run_cli_with_input_pipe x "echo foo" set key]
assert_equal "foo\n" [r get key]
assert_equal "OK" [run_cli_with_input_pipe X "echo foo" set key2 tag]
assert_equal "foo\n" [r get key2]
}
test_tty_cli "Read last argument from file" {
set tmpfile [write_tmpfile "from file"]
assert_equal "OK" [run_cli_with_input_file x $tmpfile set key]
assert_equal "from file" [r get key]
assert_equal "OK" [run_cli_with_input_file X $tmpfile set key2 tag]
assert_equal "from file" [r get key2]
file delete $tmpfile
}
test_nontty_cli "Status reply" {
2010-08-25 08:15:41 -04:00
assert_equal "OK" [run_cli set key bar]
assert_equal "bar" [r get key]
}
test_nontty_cli "Integer reply" {
r del counter
2010-08-25 08:15:41 -04:00
assert_equal "1" [run_cli incr counter]
}
test_nontty_cli "Bulk reply" {
r set key "tab\tnewline\n"
assert_equal "tab\tnewline" [run_cli get key]
}
test_nontty_cli "Multi-bulk reply" {
r del list
r rpush list foo
r rpush list bar
2010-08-25 08:15:41 -04:00
assert_equal "foo\nbar" [run_cli lrange list 0 -1]
}
if {!$::tls} { ;# fake_redis_node doesn't support TLS
test_nontty_cli "ASK redirect test" {
# Set up two fake Redis nodes.
set tclsh [info nameofexecutable]
set script "tests/helpers/fake_redis_node.tcl"
set port1 [find_available_port $::baseport $::portcount]
set port2 [find_available_port $::baseport $::portcount]
set p1 [exec $tclsh $script $port1 \
"SET foo bar" "-ASK 12182 127.0.0.1:$port2" &]
set p2 [exec $tclsh $script $port2 \
"ASKING" "+OK" \
"SET foo bar" "+OK" &]
# Make sure both fake nodes have started listening
wait_for_condition 50 50 {
[catch {close [socket "127.0.0.1" $port1]}] == 0 && \
[catch {close [socket "127.0.0.1" $port2]}] == 0
} else {
fail "Failed to start fake Redis nodes"
}
# Run the cli
assert_equal "OK" [run_cli_host_port_db "127.0.0.1" $port1 0 -c SET foo bar]
}
}
test_nontty_cli "Quoted input arguments" {
r set "\x00\x00" "value"
assert_equal "value" [run_cli --quoted-input get {"\x00\x00"}]
}
test_nontty_cli "No accidental unquoting of input arguments" {
run_cli --quoted-input set {"\x41\x41"} quoted-val
run_cli set {"\x41\x41"} unquoted-val
assert_equal "quoted-val" [r get AA]
assert_equal "unquoted-val" [r get {"\x41\x41"}]
}
test_nontty_cli "Invalid quoted input arguments" {
catch {run_cli --quoted-input set {"Unterminated}} err
assert_match {*exited abnormally*} $err
# A single arg that unquotes to two arguments is also not expected
catch {run_cli --quoted-input set {"arg1" "arg2"}} err
assert_match {*exited abnormally*} $err
}
test_nontty_cli "Read last argument from pipe" {
assert_equal "OK" [run_cli_with_input_pipe x "echo foo" set key]
assert_equal "foo\n" [r get key]
assert_equal "OK" [run_cli_with_input_pipe X "echo foo" set key2 tag]
assert_equal "foo\n" [r get key2]
}
test_nontty_cli "Read last argument from file" {
set tmpfile [write_tmpfile "from file"]
assert_equal "OK" [run_cli_with_input_file x $tmpfile set key]
assert_equal "from file" [r get key]
assert_equal "OK" [run_cli_with_input_file X $tmpfile set key2 tag]
assert_equal "from file" [r get key2]
file delete $tmpfile
}
proc test_redis_cli_rdb_dump {functions_only} {
r flushdb
r function flush
set dir [lindex [r config get dir] 1]
assert_equal "OK" [r debug populate 100000 key 1000]
Redis Function Libraries (#10004) # Redis Function Libraries This PR implements Redis Functions Libraries as describe on: https://github.com/redis/redis/issues/9906. Libraries purpose is to provide a better code sharing between functions by allowing to create multiple functions in a single command. Functions that were created together can safely share code between each other without worrying about compatibility issues and versioning. Creating a new library is done using 'FUNCTION LOAD' command (full API is described below) This PR introduces a new struct called libraryInfo, libraryInfo holds information about a library: * name - name of the library * engine - engine used to create the library * code - library code * description - library description * functions - the functions exposed by the library When Redis gets the `FUNCTION LOAD` command it creates a new empty libraryInfo. Redis passes the `CODE` to the relevant engine alongside the empty libraryInfo. As a result, the engine will create one or more functions by calling 'libraryCreateFunction'. The new funcion will be added to the newly created libraryInfo. So far Everything is happening locally on the libraryInfo so it is easy to abort the operation (in case of an error) by simply freeing the libraryInfo. After the library info is fully constructed we start the joining phase by which we will join the new library to the other libraries currently exist on Redis. The joining phase make sure there is no function collision and add the library to the librariesCtx (renamed from functionCtx). LibrariesCtx is used all around the code in the exact same way as functionCtx was used (with respect to RDB loading, replicatio, ...). The only difference is that apart from function dictionary (maps function name to functionInfo object), the librariesCtx contains also a libraries dictionary that maps library name to libraryInfo object. ## New API ### FUNCTION LOAD `FUNCTION LOAD <ENGINE> <LIBRARY NAME> [REPLACE] [DESCRIPTION <DESCRIPTION>] <CODE>` Create a new library with the given parameters: * ENGINE - REPLACE Engine name to use to create the library. * LIBRARY NAME - The new library name. * REPLACE - If the library already exists, replace it. * DESCRIPTION - Library description. * CODE - Library code. Return "OK" on success, or error on the following cases: * Library name already taken and REPLACE was not used * Name collision with another existing library (even if replace was uses) * Library registration failed by the engine (usually compilation error) ## Changed API ### FUNCTION LIST `FUNCTION LIST [LIBRARYNAME <LIBRARY NAME PATTERN>] [WITHCODE]` Command was modified to also allow getting libraries code (so `FUNCTION INFO` command is no longer needed and removed). In addition the command gets an option argument, `LIBRARYNAME` allows you to only get libraries that match the given `LIBRARYNAME` pattern. By default, it returns all libraries. ### INFO MEMORY Added number of libraries to `INFO MEMORY` ### Commands flags `DENYOOM` flag was set on `FUNCTION LOAD` and `FUNCTION RESTORE`. We consider those commands as commands that add new data to the dateset (functions are data) and so we want to disallows to run those commands on OOM. ## Removed API * FUNCTION CREATE - Decided on https://github.com/redis/redis/issues/9906 * FUNCTION INFO - Decided on https://github.com/redis/redis/issues/9899 ## Lua engine changes When the Lua engine gets the code given on `FUNCTION LOAD` command, it immediately runs it, we call this run the loading run. Loading run is not a usual script run, it is not possible to invoke any Redis command from within the load run. Instead there is a new API provided by `library` object. The new API's: * `redis.log` - behave the same as `redis.log` * `redis.register_function` - register a new function to the library The loading run purpose is to register functions using the new `redis.register_function` API. Any attempt to use any other API will result in an error. In addition, the load run is has a time limit of 500ms, error is raise on timeout and the entire operation is aborted. ### `redis.register_function` `redis.register_function(<function_name>, <callback>, [<description>])` This new API allows users to register a new function that will be linked to the newly created library. This API can only be called during the load run (see definition above). Any attempt to use it outside of the load run will result in an error. The parameters pass to the API are: * function_name - Function name (must be a Lua string) * callback - Lua function object that will be called when the function is invokes using fcall/fcall_ro * description - Function description, optional (must be a Lua string). ### Example The following example creates a library called `lib` with 2 functions, `f1` and `f1`, returns 1 and 2 respectively: ``` local function f1(keys, args)     return 1 end local function f2(keys, args)     return 2 end redis.register_function('f1', f1) redis.register_function('f2', f2) ``` Notice: Unlike `eval`, functions inside a library get the KEYS and ARGV as arguments to the functions and not as global. ### Technical Details On the load run we only want the user to be able to call a white list on API's. This way, in the future, if new API's will be added, the new API's will not be available to the load run unless specifically added to this white list. We put the while list on the `library` object and make sure the `library` object is only available to the load run by using [lua_setfenv](https://www.lua.org/manual/5.1/manual.html#lua_setfenv) API. This API allows us to set the `globals` of a function (and all the function it creates). Before starting the load run we create a new fresh Lua table (call it `g`) that only contains the `library` API (we make sure to set global protection on this table just like the general global protection already exists today), then we use [lua_setfenv](https://www.lua.org/manual/5.1/manual.html#lua_setfenv) to set `g` as the global table of the load run. After the load run finished we update `g` metatable and set `__index` and `__newindex` functions to be `_G` (Lua default globals), we also pop out the `library` object as we do not need it anymore. This way, any function that was created on the load run (and will be invoke using `fcall`) will see the default globals as it expected to see them and will not have the `library` API anymore. An important outcome of this new approach is that now we can achieve a distinct global table for each library (it is not yet like that but it is very easy to achieve it now). In the future we can decide to remove global protection because global on different libraries will not collide or we can chose to give different API to different libraries base on some configuration or input. Notice that this technique was meant to prevent errors and was not meant to prevent malicious user from exploit it. For example, the load run can still save the `library` object on some local variable and then using in `fcall` context. To prevent such a malicious use, the C code also make sure it is running in the right context and if not raise an error.
2022-01-06 06:39:38 -05:00
assert_equal "OK" [r function load lua lib1 "redis.register_function('func1', function() return 123 end)"]
if {$functions_only} {
set args "--functions-rdb $dir/cli.rdb"
} else {
set args "--rdb $dir/cli.rdb"
}
catch {run_cli {*}$args} output
assert_match {*Transfer finished with success*} $output
file delete "$dir/dump.rdb"
file rename "$dir/cli.rdb" "$dir/dump.rdb"
assert_equal "OK" [r set should-not-exist 1]
Redis Function Libraries (#10004) # Redis Function Libraries This PR implements Redis Functions Libraries as describe on: https://github.com/redis/redis/issues/9906. Libraries purpose is to provide a better code sharing between functions by allowing to create multiple functions in a single command. Functions that were created together can safely share code between each other without worrying about compatibility issues and versioning. Creating a new library is done using 'FUNCTION LOAD' command (full API is described below) This PR introduces a new struct called libraryInfo, libraryInfo holds information about a library: * name - name of the library * engine - engine used to create the library * code - library code * description - library description * functions - the functions exposed by the library When Redis gets the `FUNCTION LOAD` command it creates a new empty libraryInfo. Redis passes the `CODE` to the relevant engine alongside the empty libraryInfo. As a result, the engine will create one or more functions by calling 'libraryCreateFunction'. The new funcion will be added to the newly created libraryInfo. So far Everything is happening locally on the libraryInfo so it is easy to abort the operation (in case of an error) by simply freeing the libraryInfo. After the library info is fully constructed we start the joining phase by which we will join the new library to the other libraries currently exist on Redis. The joining phase make sure there is no function collision and add the library to the librariesCtx (renamed from functionCtx). LibrariesCtx is used all around the code in the exact same way as functionCtx was used (with respect to RDB loading, replicatio, ...). The only difference is that apart from function dictionary (maps function name to functionInfo object), the librariesCtx contains also a libraries dictionary that maps library name to libraryInfo object. ## New API ### FUNCTION LOAD `FUNCTION LOAD <ENGINE> <LIBRARY NAME> [REPLACE] [DESCRIPTION <DESCRIPTION>] <CODE>` Create a new library with the given parameters: * ENGINE - REPLACE Engine name to use to create the library. * LIBRARY NAME - The new library name. * REPLACE - If the library already exists, replace it. * DESCRIPTION - Library description. * CODE - Library code. Return "OK" on success, or error on the following cases: * Library name already taken and REPLACE was not used * Name collision with another existing library (even if replace was uses) * Library registration failed by the engine (usually compilation error) ## Changed API ### FUNCTION LIST `FUNCTION LIST [LIBRARYNAME <LIBRARY NAME PATTERN>] [WITHCODE]` Command was modified to also allow getting libraries code (so `FUNCTION INFO` command is no longer needed and removed). In addition the command gets an option argument, `LIBRARYNAME` allows you to only get libraries that match the given `LIBRARYNAME` pattern. By default, it returns all libraries. ### INFO MEMORY Added number of libraries to `INFO MEMORY` ### Commands flags `DENYOOM` flag was set on `FUNCTION LOAD` and `FUNCTION RESTORE`. We consider those commands as commands that add new data to the dateset (functions are data) and so we want to disallows to run those commands on OOM. ## Removed API * FUNCTION CREATE - Decided on https://github.com/redis/redis/issues/9906 * FUNCTION INFO - Decided on https://github.com/redis/redis/issues/9899 ## Lua engine changes When the Lua engine gets the code given on `FUNCTION LOAD` command, it immediately runs it, we call this run the loading run. Loading run is not a usual script run, it is not possible to invoke any Redis command from within the load run. Instead there is a new API provided by `library` object. The new API's: * `redis.log` - behave the same as `redis.log` * `redis.register_function` - register a new function to the library The loading run purpose is to register functions using the new `redis.register_function` API. Any attempt to use any other API will result in an error. In addition, the load run is has a time limit of 500ms, error is raise on timeout and the entire operation is aborted. ### `redis.register_function` `redis.register_function(<function_name>, <callback>, [<description>])` This new API allows users to register a new function that will be linked to the newly created library. This API can only be called during the load run (see definition above). Any attempt to use it outside of the load run will result in an error. The parameters pass to the API are: * function_name - Function name (must be a Lua string) * callback - Lua function object that will be called when the function is invokes using fcall/fcall_ro * description - Function description, optional (must be a Lua string). ### Example The following example creates a library called `lib` with 2 functions, `f1` and `f1`, returns 1 and 2 respectively: ``` local function f1(keys, args)     return 1 end local function f2(keys, args)     return 2 end redis.register_function('f1', f1) redis.register_function('f2', f2) ``` Notice: Unlike `eval`, functions inside a library get the KEYS and ARGV as arguments to the functions and not as global. ### Technical Details On the load run we only want the user to be able to call a white list on API's. This way, in the future, if new API's will be added, the new API's will not be available to the load run unless specifically added to this white list. We put the while list on the `library` object and make sure the `library` object is only available to the load run by using [lua_setfenv](https://www.lua.org/manual/5.1/manual.html#lua_setfenv) API. This API allows us to set the `globals` of a function (and all the function it creates). Before starting the load run we create a new fresh Lua table (call it `g`) that only contains the `library` API (we make sure to set global protection on this table just like the general global protection already exists today), then we use [lua_setfenv](https://www.lua.org/manual/5.1/manual.html#lua_setfenv) to set `g` as the global table of the load run. After the load run finished we update `g` metatable and set `__index` and `__newindex` functions to be `_G` (Lua default globals), we also pop out the `library` object as we do not need it anymore. This way, any function that was created on the load run (and will be invoke using `fcall`) will see the default globals as it expected to see them and will not have the `library` API anymore. An important outcome of this new approach is that now we can achieve a distinct global table for each library (it is not yet like that but it is very easy to achieve it now). In the future we can decide to remove global protection because global on different libraries will not collide or we can chose to give different API to different libraries base on some configuration or input. Notice that this technique was meant to prevent errors and was not meant to prevent malicious user from exploit it. For example, the load run can still save the `library` object on some local variable and then using in `fcall` context. To prevent such a malicious use, the C code also make sure it is running in the right context and if not raise an error.
2022-01-06 06:39:38 -05:00
assert_equal "OK" [r function load lua should_not_exist_func "redis.register_function('should_not_exist_func', function() return 456 end)"]
assert_equal "OK" [r debug reload nosave]
assert_equal {} [r get should-not-exist]
Function Flags support (no-writes, no-cluster, allow-state, allow-oom) (#10066) # Redis Functions Flags Following the discussion on #10025 Added Functions Flags support. The PR is divided to 2 sections: * Add named argument support to `redis.register_function` API. * Add support for function flags ## `redis.register_function` named argument support The first part of the PR adds support for named argument on `redis.register_function`, example: ``` redis.register_function{ function_name='f1', callback=function() return 'hello' end, description='some desc' } ``` The positional arguments is also kept, which means that it still possible to write: ``` redis.register_function('f1', function() return 'hello' end) ``` But notice that it is no longer possible to pass the optional description argument on the positional argument version. Positional argument was change to allow passing only the mandatory arguments (function name and callback). To pass more arguments the user must use the named argument version. As with positional arguments, the `function_name` and `callback` is mandatory and an error will be raise if those are missing. Also, an error will be raise if an unknown argument name is given or the arguments type is wrong. Tests was added to verify the new syntax. ## Functions Flags The second part of the PR is adding functions flags support. Flags are given to Redis when the engine calls `functionLibCreateFunction`, supported flags are: * `no-writes` - indicating the function perform no writes which means that it is OK to run it on: * read-only replica * Using FCALL_RO * If disk error detected It will not be possible to run a function in those situations unless the function turns on the `no-writes` flag * `allow-oom` - indicate that its OK to run the function even if Redis is in OOM state, if the function will not turn on this flag it will not be possible to run it if OOM reached (even if the function declares `no-writes` and even if `fcall_ro` is used). If this flag is set, any command will be allow on OOM (even those that is marked with CMD_DENYOOM). The assumption is that this flag is for advance users that knows its meaning and understand what they are doing, and Redis trust them to not increase the memory usage. (e.g. it could be an INCR or a modification on an existing key, or a DEL command) * `allow-state` - indicate that its OK to run the function on stale replica, in this case we will also make sure the function is only perform `stale` commands and raise an error if not. * `no-cluster` - indicate to disallow running the function if cluster is enabled. Default behaviure of functions (if no flags is given): 1. Allow functions to read and write 2. Do not run functions on OOM 3. Do not run functions on stale replica 4. Allow functions on cluster ### Lua API for functions flags On Lua engine, it is possible to give functions flags as `flags` named argument: ``` redis.register_function{function_name='f1', callback=function() return 1 end, flags={'no-writes', 'allow-oom'}, description='description'} ``` The function flags argument must be a Lua table that contains all the requested flags, The following will result in an error: * Unknown flag * Wrong flag type Default behaviour is the same as if no flags are used. Tests were added to verify all flags functionality ## Additional changes * mark FCALL and FCALL_RO with CMD_STALE flag (unlike EVAL), so that they can run if the function was registered with the `allow-stale` flag. * Verify `CMD_STALE` on `scriptCall` (`redis.call`), so it will not be possible to call commands from script while stale unless the command is marked with the `CMD_STALE` flags. so that even if the function is allowed while stale we do not allow it to bypass the `CMD_STALE` flag of commands. * Flags section was added to `FUNCTION LIST` command to provide the set of flags for each function: ``` > FUNCTION list withcode 1) 1) "library_name" 2) "test" 3) "engine" 4) "LUA" 5) "description" 6) (nil) 7) "functions" 8) 1) 1) "name" 2) "f1" 3) "description" 4) (nil) 5) "flags" 6) (empty array) 9) "library_code" 10) "redis.register_function{function_name='f1', callback=function() return 1 end}" ``` * Added API to get Redis version from within a script, The redis version can be provided using: 1. `redis.REDIS_VERSION` - string representation of the redis version in the format of MAJOR.MINOR.PATH 2. `redis.REDIS_VERSION_NUM` - number representation of the redis version in the format of `0x00MMmmpp` (`MM` - major, `mm` - minor, `pp` - patch). The number version can be used to check if version is greater or less another version. The string version can be used to return to the user or print as logs. This new API is provided to eval scripts and functions, it also possible to use this API during functions loading phase.
2022-01-14 07:02:02 -05:00
assert_equal {{library_name lib1 engine LUA description {} functions {{name func1 description {} flags {}}}}} [r function list]
if {$functions_only} {
assert_equal 0 [r dbsize]
} else {
assert_equal 100000 [r dbsize]
}
}
foreach {functions_only} {no yes} {
test "Dumping an RDB - functions only: $functions_only" {
# Disk-based master
assert_match "OK" [r config set repl-diskless-sync no]
test_redis_cli_rdb_dump $functions_only
# Disk-less master
assert_match "OK" [r config set repl-diskless-sync yes]
assert_match "OK" [r config set repl-diskless-sync-delay 0]
test_redis_cli_rdb_dump $functions_only
} {} {needs:repl needs:debug}
} ;# foreach functions_only
test "Scan mode" {
r flushdb
populate 1000 key: 1
# basic use
assert_equal 1000 [llength [split [run_cli --scan]]]
# pattern
assert_equal {key:2} [run_cli --scan --pattern "*:2"]
# pattern matching with a quoted string
assert_equal {key:2} [run_cli --scan --quoted-pattern {"*:\x32"}]
}
proc test_redis_cli_repl {} {
set fd [open_cli "--replica"]
wait_for_condition 500 100 {
[string match {*slave0:*state=online*} [r info]]
} else {
fail "redis-cli --replica did not connect"
}
for {set i 0} {$i < 100} {incr i} {
r set test-key test-value-$i
}
wait_for_condition 500 100 {
[string match {*test-value-99*} [read_cli $fd]]
} else {
fail "redis-cli --replica didn't read commands"
}
fconfigure $fd -blocking true
r client kill type slave
catch { close_cli $fd } err
assert_match {*Server closed the connection*} $err
}
test "Connecting as a replica" {
# Disk-based master
assert_match "OK" [r config set repl-diskless-sync no]
test_redis_cli_repl
# Disk-less master
assert_match "OK" [r config set repl-diskless-sync yes]
assert_match "OK" [r config set repl-diskless-sync-delay 0]
test_redis_cli_repl
Improve test suite to handle external servers better. (#9033) This commit revives the improves the ability to run the test suite against external servers, instead of launching and managing `redis-server` processes as part of the test fixture. This capability existed in the past, using the `--host` and `--port` options. However, it was quite limited and mostly useful when running a specific tests. Attempting to run larger chunks of the test suite experienced many issues: * Many tests depend on being able to start and control `redis-server` themselves, and there's no clear distinction between external server compatible and other tests. * Cluster mode is not supported (resulting with `CROSSSLOT` errors). This PR cleans up many things and makes it possible to run the entire test suite against an external server. It also provides more fine grained controls to handle cases where the external server supports a subset of the Redis commands, limited number of databases, cluster mode, etc. The tests directory now contains a `README.md` file that describes how this works. This commit also includes additional cleanups and fixes: * Tests can now be tagged. * Tag-based selection is now unified across `start_server`, `tags` and `test`. * More information is provided about skipped or ignored tests. * Repeated patterns in tests have been extracted to common procedures, both at a global level and on a per-test file basis. * Cleaned up some cases where test setup was based on a previous test executing (a major anti-pattern that repeats itself in many places). * Cleaned up some cases where test teardown was not part of a test (in the future we should have dedicated teardown code that executes even when tests fail). * Fixed some tests that were flaky running on external servers.
2021-06-09 08:13:24 -04:00
} {} {needs:repl}
test "Piping raw protocol" {
set cmds [tmpfile "cli_cmds"]
set cmds_fd [open $cmds "w"]
Improve test suite to handle external servers better. (#9033) This commit revives the improves the ability to run the test suite against external servers, instead of launching and managing `redis-server` processes as part of the test fixture. This capability existed in the past, using the `--host` and `--port` options. However, it was quite limited and mostly useful when running a specific tests. Attempting to run larger chunks of the test suite experienced many issues: * Many tests depend on being able to start and control `redis-server` themselves, and there's no clear distinction between external server compatible and other tests. * Cluster mode is not supported (resulting with `CROSSSLOT` errors). This PR cleans up many things and makes it possible to run the entire test suite against an external server. It also provides more fine grained controls to handle cases where the external server supports a subset of the Redis commands, limited number of databases, cluster mode, etc. The tests directory now contains a `README.md` file that describes how this works. This commit also includes additional cleanups and fixes: * Tests can now be tagged. * Tag-based selection is now unified across `start_server`, `tags` and `test`. * More information is provided about skipped or ignored tests. * Repeated patterns in tests have been extracted to common procedures, both at a global level and on a per-test file basis. * Cleaned up some cases where test setup was based on a previous test executing (a major anti-pattern that repeats itself in many places). * Cleaned up some cases where test teardown was not part of a test (in the future we should have dedicated teardown code that executes even when tests fail). * Fixed some tests that were flaky running on external servers.
2021-06-09 08:13:24 -04:00
set cmds_count 2101
if {!$::singledb} {
puts $cmds_fd [formatCommand select 9]
incr cmds_count
}
puts $cmds_fd [formatCommand del test-counter]
for {set i 0} {$i < 1000} {incr i} {
puts $cmds_fd [formatCommand incr test-counter]
puts $cmds_fd [formatCommand set large-key [string repeat "x" 20000]]
}
for {set i 0} {$i < 100} {incr i} {
puts $cmds_fd [formatCommand set very-large-key [string repeat "x" 512000]]
}
close $cmds_fd
set cli_fd [open_cli "--pipe" $cmds]
fconfigure $cli_fd -blocking true
set output [read_cli $cli_fd]
assert_equal {1000} [r get test-counter]
Improve test suite to handle external servers better. (#9033) This commit revives the improves the ability to run the test suite against external servers, instead of launching and managing `redis-server` processes as part of the test fixture. This capability existed in the past, using the `--host` and `--port` options. However, it was quite limited and mostly useful when running a specific tests. Attempting to run larger chunks of the test suite experienced many issues: * Many tests depend on being able to start and control `redis-server` themselves, and there's no clear distinction between external server compatible and other tests. * Cluster mode is not supported (resulting with `CROSSSLOT` errors). This PR cleans up many things and makes it possible to run the entire test suite against an external server. It also provides more fine grained controls to handle cases where the external server supports a subset of the Redis commands, limited number of databases, cluster mode, etc. The tests directory now contains a `README.md` file that describes how this works. This commit also includes additional cleanups and fixes: * Tests can now be tagged. * Tag-based selection is now unified across `start_server`, `tags` and `test`. * More information is provided about skipped or ignored tests. * Repeated patterns in tests have been extracted to common procedures, both at a global level and on a per-test file basis. * Cleaned up some cases where test setup was based on a previous test executing (a major anti-pattern that repeats itself in many places). * Cleaned up some cases where test teardown was not part of a test (in the future we should have dedicated teardown code that executes even when tests fail). * Fixed some tests that were flaky running on external servers.
2021-06-09 08:13:24 -04:00
assert_match "*All data transferred*errors: 0*replies: ${cmds_count}*" $output
file delete $cmds
}
test "Options -X with illegal argument" {
assert_error "*-x and -X are mutually exclusive*" {run_cli -x -X tag}
assert_error "*Unrecognized option or bad number*" {run_cli -X}
assert_error "*tag not match*" {run_cli_with_input_pipe X "echo foo" set key wrong_tag}
}
test "DUMP RESTORE with -x option" {
set cmdline [rediscli [srv host] [srv port]]
exec {*}$cmdline DEL set new_set
exec {*}$cmdline SADD set 1 2 3 4 5 6
assert_equal 6 [exec {*}$cmdline SCARD set]
assert_equal "OK" [exec {*}$cmdline -D "" --raw DUMP set | \
{*}$cmdline -x RESTORE new_set 0]
assert_equal 6 [exec {*}$cmdline SCARD new_set]
assert_equal "1\n2\n3\n4\n5\n6" [exec {*}$cmdline SMEMBERS new_set]
}
test "DUMP RESTORE with -X option" {
set cmdline [rediscli [srv host] [srv port]]
exec {*}$cmdline DEL zset new_zset
exec {*}$cmdline ZADD zset 1 a 2 b 3 c
assert_equal 3 [exec {*}$cmdline ZCARD zset]
assert_equal "OK" [exec {*}$cmdline -D "" --raw DUMP zset | \
{*}$cmdline -X dump_tag RESTORE new_zset 0 dump_tag REPLACE]
assert_equal 3 [exec {*}$cmdline ZCARD new_zset]
assert_equal "a\n1\nb\n2\nc\n3" [exec {*}$cmdline ZRANGE new_zset 0 -1 WITHSCORES]
}
}