mirror of
https://codeberg.org/redict/redict.git
synced 2025-01-23 16:48:27 -05:00
df55861838
The important part is that read-only scripts (not just EVAL_RO and FCALL_RO, but also ones with `no-writes` executed by normal EVAL or FCALL), will now be permitted to run during CLIENT PAUSE WRITE (unlike before where only the _RO commands would be processed). Other than that, some errors like OOM, READONLY, MASTERDOWN are now handled by processCommand, rather than the command itself affects the error string (and even error code in some cases), and command stats. Besides that, now the `may-replicate` commands, PFCOUNT and PUBLISH, will be considered `write` commands in scripts and will be blocked in all read-only scripts just like other write commands. They'll also be blocked in EVAL_RO (i.e. even for scripts without the `no-writes` shebang flag. This commit also hides the `may_replicate` flag from the COMMAND command output. this is a **breaking change**. background about may_replicate: We don't want to expose a no-may-replicate flag or alike to scripts, since we consider the may-replicate thing an internal concern of redis, that we may some day get rid of. In fact, the may-replicate flag was initially introduced to flag EVAL: since we didn't know what it's gonna do ahead of execution, before function-flags existed). PUBLISH and PFCOUNT, both of which because they have side effects which may some day be fixed differently. code changes: The changes in eval.c are mostly code re-ordering: - evalCalcFunctionName is extracted out of evalGenericCommand - evalExtractShebangFlags is extracted luaCreateFunction - evalGetCommandFlags is new code
1225 lines
42 KiB
Tcl
1225 lines
42 KiB
Tcl
proc get_function_code {args} {
|
|
return [format "#!%s name=%s\nredis.register_function('%s', function(KEYS, ARGV)\n %s \nend)" [lindex $args 0] [lindex $args 1] [lindex $args 2] [lindex $args 3]]
|
|
}
|
|
|
|
proc get_no_writes_function_code {args} {
|
|
return [format "#!%s name=%s\nredis.register_function{function_name='%s', callback=function(KEYS, ARGV)\n %s \nend, flags={'no-writes'}}" [lindex $args 0] [lindex $args 1] [lindex $args 2] [lindex $args 3]]
|
|
}
|
|
|
|
start_server {tags {"scripting"}} {
|
|
test {FUNCTION - Basic usage} {
|
|
r function load [get_function_code LUA test test {return 'hello'}]
|
|
r fcall test 0
|
|
} {hello}
|
|
|
|
test {FUNCTION - Load with unknown argument} {
|
|
catch {
|
|
r function load foo bar [get_function_code LUA test test {return 'hello'}]
|
|
} e
|
|
set _ $e
|
|
} {*Unknown option given*}
|
|
|
|
test {FUNCTION - Create an already exiting library raise error} {
|
|
catch {
|
|
r function load [get_function_code LUA test test {return 'hello1'}]
|
|
} e
|
|
set _ $e
|
|
} {*already exists*}
|
|
|
|
test {FUNCTION - Create an already exiting library raise error (case insensitive)} {
|
|
catch {
|
|
r function load [get_function_code LUA test test {return 'hello1'}]
|
|
} e
|
|
set _ $e
|
|
} {*already exists*}
|
|
|
|
test {FUNCTION - Create a library with wrong name format} {
|
|
catch {
|
|
r function load [get_function_code LUA {bad\0foramat} test {return 'hello1'}]
|
|
} e
|
|
set _ $e
|
|
} {*Library names can only contain letters and numbers*}
|
|
|
|
test {FUNCTION - Create library with unexisting engine} {
|
|
catch {
|
|
r function load [get_function_code bad_engine test test {return 'hello1'}]
|
|
} e
|
|
set _ $e
|
|
} {*Engine 'bad_engine' not found*}
|
|
|
|
test {FUNCTION - Test uncompiled script} {
|
|
catch {
|
|
r function load replace [get_function_code LUA test test {bad script}]
|
|
} e
|
|
set _ $e
|
|
} {*Error compiling function*}
|
|
|
|
test {FUNCTION - test replace argument} {
|
|
r function load REPLACE [get_function_code LUA test test {return 'hello1'}]
|
|
r fcall test 0
|
|
} {hello1}
|
|
|
|
test {FUNCTION - test function case insensitive} {
|
|
r fcall TEST 0
|
|
} {hello1}
|
|
|
|
test {FUNCTION - test replace argument with failure keeps old libraries} {
|
|
catch {r function create LUA test REPLACE {error}}
|
|
r fcall test 0
|
|
} {hello1}
|
|
|
|
test {FUNCTION - test function delete} {
|
|
r function delete test
|
|
catch {
|
|
r fcall test 0
|
|
} e
|
|
set _ $e
|
|
} {*Function not found*}
|
|
|
|
test {FUNCTION - test fcall bad arguments} {
|
|
r function load [get_function_code LUA test test {return 'hello'}]
|
|
catch {
|
|
r fcall test bad_arg
|
|
} e
|
|
set _ $e
|
|
} {*Bad number of keys provided*}
|
|
|
|
test {FUNCTION - test fcall bad number of keys arguments} {
|
|
catch {
|
|
r fcall test 10 key1
|
|
} e
|
|
set _ $e
|
|
} {*Number of keys can't be greater than number of args*}
|
|
|
|
test {FUNCTION - test fcall negative number of keys} {
|
|
catch {
|
|
r fcall test -1 key1
|
|
} e
|
|
set _ $e
|
|
} {*Number of keys can't be negative*}
|
|
|
|
test {FUNCTION - test delete on not exiting library} {
|
|
catch {
|
|
r function delete test1
|
|
} e
|
|
set _ $e
|
|
} {*Library not found*}
|
|
|
|
test {FUNCTION - test function kill when function is not running} {
|
|
catch {
|
|
r function kill
|
|
} e
|
|
set _ $e
|
|
} {*No scripts in execution*}
|
|
|
|
test {FUNCTION - test wrong subcommand} {
|
|
catch {
|
|
r function bad_subcommand
|
|
} e
|
|
set _ $e
|
|
} {*unknown subcommand*}
|
|
|
|
test {FUNCTION - test loading from rdb} {
|
|
r debug reload
|
|
r fcall test 0
|
|
} {hello} {needs:debug}
|
|
|
|
test {FUNCTION - test debug reload different options} {
|
|
catch {r debug reload noflush} e
|
|
assert_match "*Error trying to load the RDB*" $e
|
|
r debug reload noflush merge
|
|
r function list
|
|
} {{library_name test engine LUA functions {{name test description {} flags {}}}}} {needs:debug}
|
|
|
|
test {FUNCTION - test debug reload with nosave and noflush} {
|
|
r function delete test
|
|
r set x 1
|
|
r function load [get_function_code LUA test1 test1 {return 'hello'}]
|
|
r debug reload
|
|
r function load [get_function_code LUA test2 test2 {return 'hello'}]
|
|
r debug reload nosave noflush merge
|
|
assert_equal [r fcall test1 0] {hello}
|
|
assert_equal [r fcall test2 0] {hello}
|
|
} {} {needs:debug}
|
|
|
|
test {FUNCTION - test flushall and flushdb do not clean functions} {
|
|
r function flush
|
|
r function load REPLACE [get_function_code lua test test {return redis.call('set', 'x', '1')}]
|
|
r flushall
|
|
r flushdb
|
|
r function list
|
|
} {{library_name test engine LUA functions {{name test description {} flags {}}}}}
|
|
|
|
test {FUNCTION - test function dump and restore} {
|
|
r function flush
|
|
r function load [get_function_code lua test test {return 'hello'}]
|
|
set e [r function dump]
|
|
r function delete test
|
|
assert_match {} [r function list]
|
|
r function restore $e
|
|
r function list
|
|
} {{library_name test engine LUA functions {{name test description {} flags {}}}}}
|
|
|
|
test {FUNCTION - test function dump and restore with flush argument} {
|
|
set e [r function dump]
|
|
r function flush
|
|
assert_match {} [r function list]
|
|
r function restore $e FLUSH
|
|
r function list
|
|
} {{library_name test engine LUA functions {{name test description {} flags {}}}}}
|
|
|
|
test {FUNCTION - test function dump and restore with append argument} {
|
|
set e [r function dump]
|
|
r function flush
|
|
assert_match {} [r function list]
|
|
r function load [get_function_code lua test test {return 'hello1'}]
|
|
catch {r function restore $e APPEND} err
|
|
assert_match {*already exists*} $err
|
|
r function flush
|
|
r function load [get_function_code lua test1 test1 {return 'hello1'}]
|
|
r function restore $e APPEND
|
|
assert_match {hello} [r fcall test 0]
|
|
assert_match {hello1} [r fcall test1 0]
|
|
}
|
|
|
|
test {FUNCTION - test function dump and restore with replace argument} {
|
|
r function flush
|
|
r function load [get_function_code LUA test test {return 'hello'}]
|
|
set e [r function dump]
|
|
r function flush
|
|
assert_match {} [r function list]
|
|
r function load [get_function_code lua test test {return 'hello1'}]
|
|
assert_match {hello1} [r fcall test 0]
|
|
r function restore $e REPLACE
|
|
assert_match {hello} [r fcall test 0]
|
|
}
|
|
|
|
test {FUNCTION - test function restore with bad payload do not drop existing functions} {
|
|
r function flush
|
|
r function load [get_function_code LUA test test {return 'hello'}]
|
|
catch {r function restore bad_payload} e
|
|
assert_match {*payload version or checksum are wrong*} $e
|
|
r function list
|
|
} {{library_name test engine LUA functions {{name test description {} flags {}}}}}
|
|
|
|
test {FUNCTION - test function restore with wrong number of arguments} {
|
|
catch {r function restore arg1 args2 arg3} e
|
|
set _ $e
|
|
} {*unknown subcommand or wrong number of arguments for 'restore'. Try FUNCTION HELP.}
|
|
|
|
test {FUNCTION - test fcall_ro with write command} {
|
|
r function load REPLACE [get_no_writes_function_code lua test test {return redis.call('set', 'x', '1')}]
|
|
catch { r fcall_ro test 0 } e
|
|
set _ $e
|
|
} {*Write commands are not allowed from read-only scripts*}
|
|
|
|
test {FUNCTION - test fcall_ro with read only commands} {
|
|
r function load REPLACE [get_no_writes_function_code lua test test {return redis.call('get', 'x')}]
|
|
r set x 1
|
|
r fcall_ro test 0
|
|
} {1}
|
|
|
|
test {FUNCTION - test keys and argv} {
|
|
r function load REPLACE [get_function_code lua test test {return redis.call('set', KEYS[1], ARGV[1])}]
|
|
r fcall test 1 x foo
|
|
r get x
|
|
} {foo}
|
|
|
|
test {FUNCTION - test command get keys on fcall} {
|
|
r COMMAND GETKEYS fcall test 1 x foo
|
|
} {x}
|
|
|
|
test {FUNCTION - test command get keys on fcall_ro} {
|
|
r COMMAND GETKEYS fcall_ro test 1 x foo
|
|
} {x}
|
|
|
|
test {FUNCTION - test function kill} {
|
|
set rd [redis_deferring_client]
|
|
r config set busy-reply-threshold 10
|
|
r function load REPLACE [get_function_code lua test test {local a = 1 while true do a = a + 1 end}]
|
|
$rd fcall test 0
|
|
after 200
|
|
catch {r ping} e
|
|
assert_match {BUSY*} $e
|
|
assert_match {running_script {name test command {fcall test 0} duration_ms *} engines {*}} [r FUNCTION STATS]
|
|
r function kill
|
|
after 200 ; # Give some time to Lua to call the hook again...
|
|
assert_equal [r ping] "PONG"
|
|
}
|
|
|
|
test {FUNCTION - test script kill not working on function} {
|
|
set rd [redis_deferring_client]
|
|
r config set busy-reply-threshold 10
|
|
r function load REPLACE [get_function_code lua test test {local a = 1 while true do a = a + 1 end}]
|
|
$rd fcall test 0
|
|
after 200
|
|
catch {r ping} e
|
|
assert_match {BUSY*} $e
|
|
catch {r script kill} e
|
|
assert_match {BUSY*} $e
|
|
r function kill
|
|
after 200 ; # Give some time to Lua to call the hook again...
|
|
assert_equal [r ping] "PONG"
|
|
}
|
|
|
|
test {FUNCTION - test function kill not working on eval} {
|
|
set rd [redis_deferring_client]
|
|
r config set busy-reply-threshold 10
|
|
$rd eval {local a = 1 while true do a = a + 1 end} 0
|
|
after 200
|
|
catch {r ping} e
|
|
assert_match {BUSY*} $e
|
|
catch {r function kill} e
|
|
assert_match {BUSY*} $e
|
|
r script kill
|
|
after 200 ; # Give some time to Lua to call the hook again...
|
|
assert_equal [r ping] "PONG"
|
|
}
|
|
|
|
test {FUNCTION - test function flush} {
|
|
r function load REPLACE [get_function_code lua test test {local a = 1 while true do a = a + 1 end}]
|
|
assert_match {{library_name test engine LUA functions {{name test description {} flags {}}}}} [r function list]
|
|
r function flush
|
|
assert_match {} [r function list]
|
|
|
|
r function load REPLACE [get_function_code lua test test {local a = 1 while true do a = a + 1 end}]
|
|
assert_match {{library_name test engine LUA functions {{name test description {} flags {}}}}} [r function list]
|
|
r function flush async
|
|
assert_match {} [r function list]
|
|
|
|
r function load REPLACE [get_function_code lua test test {local a = 1 while true do a = a + 1 end}]
|
|
assert_match {{library_name test engine LUA functions {{name test description {} flags {}}}}} [r function list]
|
|
r function flush sync
|
|
assert_match {} [r function list]
|
|
}
|
|
|
|
test {FUNCTION - test function wrong argument} {
|
|
catch {r function flush bad_arg} e
|
|
assert_match {*only supports SYNC|ASYNC*} $e
|
|
|
|
catch {r function flush sync extra_arg} e
|
|
assert_match {*unknown subcommand or wrong number of arguments for 'flush'. Try FUNCTION HELP.} $e
|
|
}
|
|
}
|
|
|
|
start_server {tags {"scripting repl external:skip"}} {
|
|
start_server {} {
|
|
test "Connect a replica to the master instance" {
|
|
r -1 slaveof [srv 0 host] [srv 0 port]
|
|
wait_for_condition 150 100 {
|
|
[s -1 role] eq {slave} &&
|
|
[string match {*master_link_status:up*} [r -1 info replication]]
|
|
} else {
|
|
fail "Can't turn the instance into a replica"
|
|
}
|
|
}
|
|
|
|
test {FUNCTION - creation is replicated to replica} {
|
|
r function load [get_no_writes_function_code LUA test test {return 'hello'}]
|
|
wait_for_condition 150 100 {
|
|
[r -1 function list] eq {{library_name test engine LUA functions {{name test description {} flags no-writes}}}}
|
|
} else {
|
|
fail "Failed waiting for function to replicate to replica"
|
|
}
|
|
}
|
|
|
|
test {FUNCTION - call on replica} {
|
|
r -1 fcall test 0
|
|
} {hello}
|
|
|
|
test {FUNCTION - restore is replicated to replica} {
|
|
set e [r function dump]
|
|
|
|
r function delete test
|
|
wait_for_condition 150 100 {
|
|
[r -1 function list] eq {}
|
|
} else {
|
|
fail "Failed waiting for function to replicate to replica"
|
|
}
|
|
|
|
assert_equal [r function restore $e] {OK}
|
|
|
|
wait_for_condition 150 100 {
|
|
[r -1 function list] eq {{library_name test engine LUA functions {{name test description {} flags no-writes}}}}
|
|
} else {
|
|
fail "Failed waiting for function to replicate to replica"
|
|
}
|
|
}
|
|
|
|
test {FUNCTION - delete is replicated to replica} {
|
|
r function delete test
|
|
wait_for_condition 150 100 {
|
|
[r -1 function list] eq {}
|
|
} else {
|
|
fail "Failed waiting for function to replicate to replica"
|
|
}
|
|
}
|
|
|
|
test {FUNCTION - flush is replicated to replica} {
|
|
r function load [get_function_code LUA test test {return 'hello'}]
|
|
wait_for_condition 150 100 {
|
|
[r -1 function list] eq {{library_name test engine LUA functions {{name test description {} flags {}}}}}
|
|
} else {
|
|
fail "Failed waiting for function to replicate to replica"
|
|
}
|
|
r function flush
|
|
wait_for_condition 150 100 {
|
|
[r -1 function list] eq {}
|
|
} else {
|
|
fail "Failed waiting for function to replicate to replica"
|
|
}
|
|
}
|
|
|
|
test "Disconnecting the replica from master instance" {
|
|
r -1 slaveof no one
|
|
# creating a function after disconnect to make sure function
|
|
# is replicated on rdb phase
|
|
r function load [get_no_writes_function_code LUA test test {return 'hello'}]
|
|
|
|
# reconnect the replica
|
|
r -1 slaveof [srv 0 host] [srv 0 port]
|
|
wait_for_condition 150 100 {
|
|
[s -1 role] eq {slave} &&
|
|
[string match {*master_link_status:up*} [r -1 info replication]]
|
|
} else {
|
|
fail "Can't turn the instance into a replica"
|
|
}
|
|
}
|
|
|
|
test "FUNCTION - test replication to replica on rdb phase" {
|
|
r -1 fcall test 0
|
|
} {hello}
|
|
|
|
test "FUNCTION - test replication to replica on rdb phase info command" {
|
|
r -1 function list
|
|
} {{library_name test engine LUA functions {{name test description {} flags no-writes}}}}
|
|
|
|
test "FUNCTION - create on read only replica" {
|
|
catch {
|
|
r -1 function load [get_function_code LUA test test {return 'hello'}]
|
|
} e
|
|
set _ $e
|
|
} {*can't write against a read only replica*}
|
|
|
|
test "FUNCTION - delete on read only replica" {
|
|
catch {
|
|
r -1 function delete test
|
|
} e
|
|
set _ $e
|
|
} {*can't write against a read only replica*}
|
|
|
|
test "FUNCTION - function effect is replicated to replica" {
|
|
r function load REPLACE [get_function_code LUA test test {return redis.call('set', 'x', '1')}]
|
|
r fcall test 0
|
|
assert {[r get x] eq {1}}
|
|
wait_for_condition 150 100 {
|
|
[r -1 get x] eq {1}
|
|
} else {
|
|
fail "Failed waiting function effect to be replicated to replica"
|
|
}
|
|
}
|
|
|
|
test "FUNCTION - modify key space of read only replica" {
|
|
catch {
|
|
r -1 fcall test 0
|
|
} e
|
|
set _ $e
|
|
} {READONLY You can't write against a read only replica.}
|
|
}
|
|
}
|
|
|
|
test {FUNCTION can processes create, delete and flush commands in AOF when doing "debug loadaof" in read-only slaves} {
|
|
start_server {} {
|
|
r config set appendonly yes
|
|
waitForBgrewriteaof r
|
|
r FUNCTION LOAD "#!lua name=test\nredis.register_function('test', function() return 'hello' end)"
|
|
r config set slave-read-only yes
|
|
r slaveof 127.0.0.1 0
|
|
r debug loadaof
|
|
r slaveof no one
|
|
assert_equal [r function list] {{library_name test engine LUA functions {{name test description {} flags {}}}}}
|
|
|
|
r FUNCTION DELETE test
|
|
|
|
r slaveof 127.0.0.1 0
|
|
r debug loadaof
|
|
r slaveof no one
|
|
assert_equal [r function list] {}
|
|
|
|
r FUNCTION LOAD "#!lua name=test\nredis.register_function('test', function() return 'hello' end)"
|
|
r FUNCTION FLUSH
|
|
|
|
r slaveof 127.0.0.1 0
|
|
r debug loadaof
|
|
r slaveof no one
|
|
assert_equal [r function list] {}
|
|
}
|
|
} {} {needs:debug external:skip}
|
|
|
|
start_server {tags {"scripting"}} {
|
|
test {LIBRARIES - test shared function can access default globals} {
|
|
r function load {#!lua name=lib1
|
|
local function ping()
|
|
return redis.call('ping')
|
|
end
|
|
redis.register_function(
|
|
'f1',
|
|
function(keys, args)
|
|
return ping()
|
|
end
|
|
)
|
|
}
|
|
r fcall f1 0
|
|
} {PONG}
|
|
|
|
test {LIBRARIES - usage and code sharing} {
|
|
r function load REPLACE {#!lua name=lib1
|
|
local function add1(a)
|
|
return a + 1
|
|
end
|
|
redis.register_function(
|
|
'f1',
|
|
function(keys, args)
|
|
return add1(1)
|
|
end
|
|
)
|
|
redis.register_function(
|
|
'f2',
|
|
function(keys, args)
|
|
return add1(2)
|
|
end
|
|
)
|
|
}
|
|
assert_equal [r fcall f1 0] {2}
|
|
assert_equal [r fcall f2 0] {3}
|
|
r function list
|
|
} {{library_name lib1 engine LUA functions {*}}}
|
|
|
|
test {LIBRARIES - test registration failure revert the entire load} {
|
|
catch {
|
|
r function load replace {#!lua name=lib1
|
|
local function add1(a)
|
|
return a + 2
|
|
end
|
|
redis.register_function(
|
|
'f1',
|
|
function(keys, args)
|
|
return add1(1)
|
|
end
|
|
)
|
|
redis.register_function(
|
|
'f2',
|
|
'not a function'
|
|
)
|
|
}
|
|
} e
|
|
assert_match {*second argument to redis.register_function must be a function*} $e
|
|
assert_equal [r fcall f1 0] {2}
|
|
assert_equal [r fcall f2 0] {3}
|
|
}
|
|
|
|
test {LIBRARIES - test registration function name collision} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
redis.register_function(
|
|
'f1',
|
|
function(keys, args)
|
|
return 1
|
|
end
|
|
)
|
|
}
|
|
} e
|
|
assert_match {*Function f1 already exists*} $e
|
|
assert_equal [r fcall f1 0] {2}
|
|
assert_equal [r fcall f2 0] {3}
|
|
}
|
|
|
|
test {LIBRARIES - test registration function name collision on same library} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
redis.register_function(
|
|
'f1',
|
|
function(keys, args)
|
|
return 1
|
|
end
|
|
)
|
|
redis.register_function(
|
|
'f1',
|
|
function(keys, args)
|
|
return 1
|
|
end
|
|
)
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*Function already exists in the library*}
|
|
|
|
test {LIBRARIES - test registration with no argument} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
redis.register_function()
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*wrong number of arguments to redis.register_function*}
|
|
|
|
test {LIBRARIES - test registration with only name} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
redis.register_function('f1')
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*calling redis.register_function with a single argument is only applicable to Lua table*}
|
|
|
|
test {LIBRARIES - test registration with to many arguments} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
redis.register_function('f1', function() return 1 end, {}, 'description', 'extra arg')
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*wrong number of arguments to redis.register_function*}
|
|
|
|
test {LIBRARIES - test registration with no string name} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
redis.register_function(nil, function() return 1 end)
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*first argument to redis.register_function must be a string*}
|
|
|
|
test {LIBRARIES - test registration with wrong name format} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
redis.register_function('test\0test', function() return 1 end)
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*Function names can only contain letters and numbers and must be at least one character long*}
|
|
|
|
test {LIBRARIES - test registration with empty name} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
redis.register_function('', function() return 1 end)
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*Function names can only contain letters and numbers and must be at least one character long*}
|
|
|
|
test {LIBRARIES - math.random from function load} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
return math.random()
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*attempted to access nonexistent global variable 'math'*}
|
|
|
|
test {LIBRARIES - redis.call from function load} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
return redis.call('ping')
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*attempted to access nonexistent global variable 'call'*}
|
|
|
|
test {LIBRARIES - redis.setresp from function load} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
return redis.setresp(3)
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*attempted to access nonexistent global variable 'setresp'*}
|
|
|
|
test {LIBRARIES - redis.set_repl from function load} {
|
|
catch {
|
|
r function load replace {#!lua name=lib2
|
|
return redis.set_repl(redis.REPL_NONE)
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*attempted to access nonexistent global variable 'set_repl'*}
|
|
|
|
test {LIBRARIES - malicious access test} {
|
|
# the 'library' API is not exposed inside a
|
|
# function context and the 'redis' API is not
|
|
# expose on the library registration context.
|
|
# But a malicious user might find a way to hack it
|
|
# (as demonstrated in this test). This is why we
|
|
# have another level of protection on the C
|
|
# code itself and we want to test it and verify
|
|
# that it works properly.
|
|
r function load replace {#!lua name=lib1
|
|
local lib = redis
|
|
lib.register_function('f1', function ()
|
|
lib.redis = redis
|
|
lib.math = math
|
|
return {ok='OK'}
|
|
end)
|
|
|
|
lib.register_function('f2', function ()
|
|
lib.register_function('f1', function ()
|
|
lib.redis = redis
|
|
lib.math = math
|
|
return {ok='OK'}
|
|
end)
|
|
end)
|
|
}
|
|
catch {[r fcall f1 0]} e
|
|
assert_match {*Attempt to modify a readonly table*} $e
|
|
|
|
catch {[r function load {#!lua name=lib2
|
|
redis.math.random()
|
|
}]} e
|
|
assert_match {*Script attempted to access nonexistent global variable 'math'*} $e
|
|
|
|
catch {[r function load {#!lua name=lib2
|
|
redis.redis.call('ping')
|
|
}]} e
|
|
assert_match {*Script attempted to access nonexistent global variable 'redis'*} $e
|
|
|
|
catch {[r fcall f2 0]} e
|
|
assert_match {*can only be called on FUNCTION LOAD command*} $e
|
|
}
|
|
|
|
test {LIBRARIES - delete removed all functions on library} {
|
|
r function delete lib1
|
|
r function list
|
|
} {}
|
|
|
|
test {LIBRARIES - register function inside a function} {
|
|
r function load {#!lua name=lib
|
|
redis.register_function(
|
|
'f1',
|
|
function(keys, args)
|
|
redis.register_function(
|
|
'f2',
|
|
function(key, args)
|
|
return 2
|
|
end
|
|
)
|
|
return 1
|
|
end
|
|
)
|
|
}
|
|
catch {r fcall f1 0} e
|
|
set _ $e
|
|
} {*attempt to call field 'register_function' (a nil value)*}
|
|
|
|
test {LIBRARIES - register library with no functions} {
|
|
r function flush
|
|
catch {
|
|
r function load {#!lua name=lib
|
|
return 1
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*No functions registered*}
|
|
|
|
test {LIBRARIES - load timeout} {
|
|
catch {
|
|
r function load {#!lua name=lib
|
|
local a = 1
|
|
while 1 do a = a + 1 end
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*FUNCTION LOAD timeout*}
|
|
|
|
test {LIBRARIES - verify global protection on the load run} {
|
|
catch {
|
|
r function load {#!lua name=lib
|
|
a = 1
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*Attempt to modify a readonly table*}
|
|
|
|
test {LIBRARIES - named arguments} {
|
|
r function load {#!lua name=lib
|
|
redis.register_function{
|
|
function_name='f1',
|
|
callback=function()
|
|
return 'hello'
|
|
end,
|
|
description='some desc'
|
|
}
|
|
}
|
|
r function list
|
|
} {{library_name lib engine LUA functions {{name f1 description {some desc} flags {}}}}}
|
|
|
|
test {LIBRARIES - named arguments, bad function name} {
|
|
catch {
|
|
r function load replace {#!lua name=lib
|
|
redis.register_function{
|
|
function_name=function() return 1 end,
|
|
callback=function()
|
|
return 'hello'
|
|
end,
|
|
description='some desc'
|
|
}
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*function_name argument given to redis.register_function must be a string*}
|
|
|
|
test {LIBRARIES - named arguments, bad callback type} {
|
|
catch {
|
|
r function load replace {#!lua name=lib
|
|
redis.register_function{
|
|
function_name='f1',
|
|
callback='bad',
|
|
description='some desc'
|
|
}
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*callback argument given to redis.register_function must be a function*}
|
|
|
|
test {LIBRARIES - named arguments, bad description} {
|
|
catch {
|
|
r function load replace {#!lua name=lib
|
|
redis.register_function{
|
|
function_name='f1',
|
|
callback=function()
|
|
return 'hello'
|
|
end,
|
|
description=function() return 1 end
|
|
}
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*description argument given to redis.register_function must be a string*}
|
|
|
|
test {LIBRARIES - named arguments, unknown argument} {
|
|
catch {
|
|
r function load replace {#!lua name=lib
|
|
redis.register_function{
|
|
function_name='f1',
|
|
callback=function()
|
|
return 'hello'
|
|
end,
|
|
description='desc',
|
|
some_unknown='unknown'
|
|
}
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*unknown argument given to redis.register_function*}
|
|
|
|
test {LIBRARIES - named arguments, missing function name} {
|
|
catch {
|
|
r function load replace {#!lua name=lib
|
|
redis.register_function{
|
|
callback=function()
|
|
return 'hello'
|
|
end,
|
|
description='desc'
|
|
}
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*redis.register_function must get a function name argument*}
|
|
|
|
test {LIBRARIES - named arguments, missing callback} {
|
|
catch {
|
|
r function load replace {#!lua name=lib
|
|
redis.register_function{
|
|
function_name='f1',
|
|
description='desc'
|
|
}
|
|
}
|
|
} e
|
|
set _ $e
|
|
} {*redis.register_function must get a callback argument*}
|
|
|
|
test {FUNCTION - test function restore with function name collision} {
|
|
r function flush
|
|
r function load {#!lua name=lib1
|
|
local function add1(a)
|
|
return a + 1
|
|
end
|
|
redis.register_function(
|
|
'f1',
|
|
function(keys, args)
|
|
return add1(1)
|
|
end
|
|
)
|
|
redis.register_function(
|
|
'f2',
|
|
function(keys, args)
|
|
return add1(2)
|
|
end
|
|
)
|
|
redis.register_function(
|
|
'f3',
|
|
function(keys, args)
|
|
return add1(3)
|
|
end
|
|
)
|
|
}
|
|
set e [r function dump]
|
|
r function flush
|
|
|
|
# load a library with different name but with the same function name
|
|
r function load {#!lua name=lib1
|
|
redis.register_function(
|
|
'f6',
|
|
function(keys, args)
|
|
return 7
|
|
end
|
|
)
|
|
}
|
|
r function load {#!lua name=lib2
|
|
local function add1(a)
|
|
return a + 1
|
|
end
|
|
redis.register_function(
|
|
'f4',
|
|
function(keys, args)
|
|
return add1(4)
|
|
end
|
|
)
|
|
redis.register_function(
|
|
'f5',
|
|
function(keys, args)
|
|
return add1(5)
|
|
end
|
|
)
|
|
redis.register_function(
|
|
'f3',
|
|
function(keys, args)
|
|
return add1(3)
|
|
end
|
|
)
|
|
}
|
|
|
|
catch {r function restore $e} error
|
|
assert_match {*Library lib1 already exists*} $error
|
|
assert_equal [r fcall f3 0] {4}
|
|
assert_equal [r fcall f4 0] {5}
|
|
assert_equal [r fcall f5 0] {6}
|
|
assert_equal [r fcall f6 0] {7}
|
|
|
|
catch {r function restore $e replace} error
|
|
assert_match {*Function f3 already exists*} $error
|
|
assert_equal [r fcall f3 0] {4}
|
|
assert_equal [r fcall f4 0] {5}
|
|
assert_equal [r fcall f5 0] {6}
|
|
assert_equal [r fcall f6 0] {7}
|
|
}
|
|
|
|
test {FUNCTION - test function list with code} {
|
|
r function flush
|
|
r function load {#!lua name=library1
|
|
redis.register_function('f6', function(keys, args) return 7 end)
|
|
}
|
|
r function list withcode
|
|
} {{library_name library1 engine LUA functions {{name f6 description {} flags {}}} library_code {*redis.register_function('f6', function(keys, args) return 7 end)*}}}
|
|
|
|
test {FUNCTION - test function list with pattern} {
|
|
r function load {#!lua name=lib1
|
|
redis.register_function('f7', function(keys, args) return 7 end)
|
|
}
|
|
r function list libraryname library*
|
|
} {{library_name library1 engine LUA functions {{name f6 description {} flags {}}}}}
|
|
|
|
test {FUNCTION - test function list wrong argument} {
|
|
catch {r function list bad_argument} e
|
|
set _ $e
|
|
} {*Unknown argument bad_argument*}
|
|
|
|
test {FUNCTION - test function list with bad argument to library name} {
|
|
catch {r function list libraryname} e
|
|
set _ $e
|
|
} {*library name argument was not given*}
|
|
|
|
test {FUNCTION - test function list withcode multiple times} {
|
|
catch {r function list withcode withcode} e
|
|
set _ $e
|
|
} {*Unknown argument withcode*}
|
|
|
|
test {FUNCTION - test function list libraryname multiple times} {
|
|
catch {r function list withcode libraryname foo libraryname foo} e
|
|
set _ $e
|
|
} {*Unknown argument libraryname*}
|
|
|
|
test {FUNCTION - verify OOM on function load and function restore} {
|
|
r function flush
|
|
r function load replace {#!lua name=test
|
|
redis.register_function('f1', function() return 1 end)
|
|
}
|
|
set payload [r function dump]
|
|
r config set maxmemory 1
|
|
|
|
r function flush
|
|
catch {r function load replace {#!lua name=test
|
|
redis.register_function('f1', function() return 1 end)
|
|
}} e
|
|
assert_match {*command not allowed when used memory*} $e
|
|
|
|
r function flush
|
|
catch {r function restore $payload} e
|
|
assert_match {*command not allowed when used memory*} $e
|
|
|
|
r config set maxmemory 0
|
|
} {OK} {needs:config-maxmemory}
|
|
|
|
test {FUNCTION - verify allow-omm allows running any command} {
|
|
r FUNCTION load replace {#!lua name=f1
|
|
redis.register_function{
|
|
function_name='f1',
|
|
callback=function() return redis.call('set', 'x', '1') end,
|
|
flags={'allow-oom'}
|
|
}
|
|
}
|
|
|
|
r config set maxmemory 1
|
|
|
|
assert_match {OK} [r fcall f1 1 x]
|
|
assert_match {1} [r get x]
|
|
|
|
r config set maxmemory 0
|
|
} {OK} {needs:config-maxmemory}
|
|
}
|
|
|
|
start_server {tags {"scripting"}} {
|
|
test {FUNCTION - wrong flags type named arguments} {
|
|
catch {r function load replace {#!lua name=test
|
|
redis.register_function{
|
|
function_name = 'f1',
|
|
callback = function() return 1 end,
|
|
flags = 'bad flags type'
|
|
}
|
|
}} e
|
|
set _ $e
|
|
} {*flags argument to redis.register_function must be a table representing function flags*}
|
|
|
|
test {FUNCTION - wrong flag type} {
|
|
catch {r function load replace {#!lua name=test
|
|
redis.register_function{
|
|
function_name = 'f1',
|
|
callback = function() return 1 end,
|
|
flags = {function() return 1 end}
|
|
}
|
|
}} e
|
|
set _ $e
|
|
} {*unknown flag given*}
|
|
|
|
test {FUNCTION - unknown flag} {
|
|
catch {r function load replace {#!lua name=test
|
|
redis.register_function{
|
|
function_name = 'f1',
|
|
callback = function() return 1 end,
|
|
flags = {'unknown'}
|
|
}
|
|
}} e
|
|
set _ $e
|
|
} {*unknown flag given*}
|
|
|
|
test {FUNCTION - write script on fcall_ro} {
|
|
r function load replace {#!lua name=test
|
|
redis.register_function{
|
|
function_name = 'f1',
|
|
callback = function() return redis.call('set', 'x', 1) end
|
|
}
|
|
}
|
|
catch {r fcall_ro f1 0} e
|
|
set _ $e
|
|
} {*Can not execute a script with write flag using \*_ro command*}
|
|
|
|
test {FUNCTION - write script with no-writes flag} {
|
|
r function load replace {#!lua name=test
|
|
redis.register_function{
|
|
function_name = 'f1',
|
|
callback = function() return redis.call('set', 'x', 1) end,
|
|
flags = {'no-writes'}
|
|
}
|
|
}
|
|
catch {r fcall f1 0} e
|
|
set _ $e
|
|
} {*Write commands are not allowed from read-only scripts*}
|
|
|
|
test {FUNCTION - deny oom} {
|
|
r FUNCTION load replace {#!lua name=test
|
|
redis.register_function('f1', function() return redis.call('set', 'x', '1') end)
|
|
}
|
|
|
|
r config set maxmemory 1
|
|
|
|
catch {[r fcall f1 1 k]} e
|
|
assert_match {OOM *when used memory > 'maxmemory'*} $e
|
|
|
|
r config set maxmemory 0
|
|
} {OK} {needs:config-maxmemory}
|
|
|
|
test {FUNCTION - deny oom on no-writes function} {
|
|
r FUNCTION load replace {#!lua name=test
|
|
redis.register_function{function_name='f1', callback=function() return 'hello' end, flags={'no-writes'}}
|
|
}
|
|
|
|
r config set maxmemory 1
|
|
|
|
assert_equal [r fcall f1 1 k] hello
|
|
assert_equal [r fcall_ro f1 1 k] hello
|
|
|
|
r config set maxmemory 0
|
|
} {OK} {needs:config-maxmemory}
|
|
|
|
test {FUNCTION - allow stale} {
|
|
r FUNCTION load replace {#!lua name=test
|
|
redis.register_function{function_name='f1', callback=function() return 'hello' end, flags={'no-writes'}}
|
|
redis.register_function{function_name='f2', callback=function() return 'hello' end, flags={'allow-stale', 'no-writes'}}
|
|
redis.register_function{function_name='f3', callback=function() return redis.call('get', 'x') end, flags={'allow-stale', 'no-writes'}}
|
|
redis.register_function{function_name='f4', callback=function() return redis.call('info', 'server') end, flags={'allow-stale', 'no-writes'}}
|
|
}
|
|
|
|
r config set replica-serve-stale-data no
|
|
r replicaof 127.0.0.1 1
|
|
|
|
catch {[r fcall f1 0]} e
|
|
assert_match {MASTERDOWN *} $e
|
|
|
|
assert_equal {hello} [r fcall f2 0]
|
|
|
|
catch {[r fcall f3 0]} e
|
|
assert_match {ERR *Can not execute the command on a stale replica*} $e
|
|
|
|
assert_match {*redis_version*} [r fcall f4 0]
|
|
|
|
r replicaof no one
|
|
r config set replica-serve-stale-data yes
|
|
set _ {}
|
|
} {} {external:skip}
|
|
|
|
test {FUNCTION - redis version api} {
|
|
r FUNCTION load replace {#!lua name=test
|
|
local version = redis.REDIS_VERSION_NUM
|
|
|
|
redis.register_function{function_name='get_version_v1', callback=function()
|
|
return string.format('%s.%s.%s',
|
|
bit.band(bit.rshift(version, 16), 0x000000ff),
|
|
bit.band(bit.rshift(version, 8), 0x000000ff),
|
|
bit.band(version, 0x000000ff))
|
|
end}
|
|
redis.register_function{function_name='get_version_v2', callback=function() return redis.REDIS_VERSION end}
|
|
}
|
|
|
|
catch {[r fcall f1 0]} e
|
|
assert_equal [r fcall get_version_v1 0] [r fcall get_version_v2 0]
|
|
}
|
|
|
|
test {FUNCTION - function stats} {
|
|
r FUNCTION FLUSH
|
|
|
|
r FUNCTION load {#!lua name=test1
|
|
redis.register_function('f1', function() return 1 end)
|
|
redis.register_function('f2', function() return 1 end)
|
|
}
|
|
|
|
r FUNCTION load {#!lua name=test2
|
|
redis.register_function('f3', function() return 1 end)
|
|
}
|
|
|
|
r function stats
|
|
} {running_script {} engines {LUA {libraries_count 2 functions_count 3}}}
|
|
|
|
test {FUNCTION - function stats reloaded correctly from rdb} {
|
|
r debug reload
|
|
r function stats
|
|
} {running_script {} engines {LUA {libraries_count 2 functions_count 3}}} {needs:debug}
|
|
|
|
test {FUNCTION - function stats delete library} {
|
|
r function delete test1
|
|
r function stats
|
|
} {running_script {} engines {LUA {libraries_count 1 functions_count 1}}}
|
|
|
|
test {FUNCTION - test function stats on loading failure} {
|
|
r FUNCTION FLUSH
|
|
|
|
r FUNCTION load {#!lua name=test1
|
|
redis.register_function('f1', function() return 1 end)
|
|
redis.register_function('f2', function() return 1 end)
|
|
}
|
|
|
|
catch {r FUNCTION load {#!lua name=test1
|
|
redis.register_function('f3', function() return 1 end)
|
|
}} e
|
|
assert_match "*Library 'test1' already exists*" $e
|
|
|
|
|
|
r function stats
|
|
} {running_script {} engines {LUA {libraries_count 1 functions_count 2}}}
|
|
|
|
test {FUNCTION - function stats cleaned after flush} {
|
|
r function flush
|
|
r function stats
|
|
} {running_script {} engines {LUA {libraries_count 0 functions_count 0}}}
|
|
|
|
test {FUNCTION - function test empty engine} {
|
|
catch {r function load replace {#! name=test
|
|
redis.register_function('foo', function() return 1 end)
|
|
}} e
|
|
set _ $e
|
|
} {ERR Engine '' not found}
|
|
|
|
test {FUNCTION - function test unknown metadata value} {
|
|
catch {r function load replace {#!lua name=test foo=bar
|
|
redis.register_function('foo', function() return 1 end)
|
|
}} e
|
|
set _ $e
|
|
} {ERR Invalid metadata value given: foo=bar}
|
|
|
|
test {FUNCTION - function test no name} {
|
|
catch {r function load replace {#!lua
|
|
redis.register_function('foo', function() return 1 end)
|
|
}} e
|
|
set _ $e
|
|
} {ERR Library name was not given}
|
|
|
|
test {FUNCTION - function test multiple names} {
|
|
catch {r function load replace {#!lua name=foo name=bar
|
|
redis.register_function('foo', function() return 1 end)
|
|
}} e
|
|
set _ $e
|
|
} {ERR Invalid metadata value, name argument was given multiple times}
|
|
|
|
test {FUNCTION - function test name with quotes} {
|
|
r function load replace {#!lua name="foo"
|
|
redis.register_function('foo', function() return 1 end)
|
|
}
|
|
} {foo}
|
|
|
|
test {FUNCTION - trick global protection 1} {
|
|
r FUNCTION FLUSH
|
|
|
|
r FUNCTION load {#!lua name=test1
|
|
redis.register_function('f1', function()
|
|
mt = getmetatable(_G)
|
|
original_globals = mt.__index
|
|
original_globals['redis'] = function() return 1 end
|
|
end)
|
|
}
|
|
|
|
catch {[r fcall f1 0]} e
|
|
set _ $e
|
|
} {*Attempt to modify a readonly table*}
|
|
|
|
test {FUNCTION - test getmetatable on script load} {
|
|
r FUNCTION FLUSH
|
|
|
|
catch {
|
|
r FUNCTION load {#!lua name=test1
|
|
mt = getmetatable(_G)
|
|
}
|
|
} e
|
|
|
|
set _ $e
|
|
} {*Script attempted to access nonexistent global variable 'getmetatable'*}
|
|
|
|
}
|