diff options
Diffstat (limited to 'examples/redis-unstable/tests/unit/scripting.tcl')
| -rw-r--r-- | examples/redis-unstable/tests/unit/scripting.tcl | 2688 |
1 files changed, 0 insertions, 2688 deletions
diff --git a/examples/redis-unstable/tests/unit/scripting.tcl b/examples/redis-unstable/tests/unit/scripting.tcl deleted file mode 100644 index 911f114..0000000 --- a/examples/redis-unstable/tests/unit/scripting.tcl +++ /dev/null @@ -1,2688 +0,0 @@ -# -# Copyright (c) 2009-Present, Redis Ltd. -# All rights reserved. -# -# Copyright (c) 2024-present, Valkey contributors. -# All rights reserved. -# -# Licensed under your choice of (a) the Redis Source Available License 2.0 -# (RSALv2); or (b) the Server Side Public License v1 (SSPLv1); or (c) the -# GNU Affero General Public License v3 (AGPLv3). -# -# Portions of this file are available under BSD3 terms; see REDISCONTRIBUTIONS for more information. -# - -foreach is_eval {0 1} { - -if {$is_eval == 1} { - proc run_script {args} { - r eval {*}$args - } - proc run_script_ro {args} { - r eval_ro {*}$args - } - proc run_script_on_connection {args} { - [lindex $args 0] eval {*}[lrange $args 1 end] - } - proc kill_script {args} { - r script kill - } -} else { - proc run_script {args} { - r function load replace [format "#!lua name=test\nredis.register_function('test', function(KEYS, ARGV)\n %s \nend)" [lindex $args 0]] - if {[r readingraw] eq 1} { - # read name - assert_equal {test} [r read] - } - r fcall test {*}[lrange $args 1 end] - } - proc run_script_ro {args} { - r function load replace [format "#!lua name=test\nredis.register_function{function_name='test', callback=function(KEYS, ARGV)\n %s \nend, flags={'no-writes'}}" [lindex $args 0]] - if {[r readingraw] eq 1} { - # read name - assert_equal {test} [r read] - } - r fcall_ro test {*}[lrange $args 1 end] - } - proc run_script_on_connection {args} { - set rd [lindex $args 0] - $rd function load replace [format "#!lua name=test\nredis.register_function('test', function(KEYS, ARGV)\n %s \nend)" [lindex $args 1]] - # read name - $rd read - $rd fcall test {*}[lrange $args 2 end] - } - proc kill_script {args} { - r function kill - } -} - -start_server {tags {"scripting"}} { - - if {$is_eval eq 1} { - test {Script - disallow write on OOM} { - r config set maxmemory 1 - - catch {[r eval "redis.call('set', 'x', 1)" 0]} e - assert_match {*command not allowed when used memory*} $e - - r config set maxmemory 0 - } {OK} {needs:config-maxmemory} - } ;# is_eval - - test {EVAL - Does Lua interpreter replies to our requests?} { - run_script {return 'hello'} 0 - } {hello} - - test {EVAL - Return _G} { - run_script {return _G} 0 - } {} - - test {EVAL - Return table with a metatable that raise error} { - run_script {local a = {}; setmetatable(a,{__index=function() foo() end}) return a} 0 - } {} - - test {EVAL - Return table with a metatable that call redis} { - run_script {local a = {}; setmetatable(a,{__index=function() redis.call('set', 'x', '1') end}) return a} 1 x - # make sure x was not set - r get x - } {} - - test {EVAL - Lua integer -> Redis protocol type conversion} { - run_script {return 100.5} 0 - } {100} - - test {EVAL - Lua string -> Redis protocol type conversion} { - run_script {return 'hello world'} 0 - } {hello world} - - test {EVAL - Lua true boolean -> Redis protocol type conversion} { - run_script {return true} 0 - } {1} - - test {EVAL - Lua false boolean -> Redis protocol type conversion} { - run_script {return false} 0 - } {} - - test {EVAL - Lua status code reply -> Redis protocol type conversion} { - run_script {return {ok='fine'}} 0 - } {fine} - - test {EVAL - Lua error reply -> Redis protocol type conversion} { - catch { - run_script {return {err='ERR this is an error'}} 0 - } e - set _ $e - } {ERR this is an error} - - test {EVAL - Lua table -> Redis protocol type conversion} { - run_script {return {1,2,3,'ciao',{1,2}}} 0 - } {1 2 3 ciao {1 2}} - - test {EVAL - Are the KEYS and ARGV arrays populated correctly?} { - run_script {return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}} 2 a{t} b{t} c{t} d{t} - } {a{t} b{t} c{t} d{t}} - - test {EVAL - is Lua able to call Redis API?} { - r set mykey myval - run_script {return redis.call('get',KEYS[1])} 1 mykey - } {myval} - - if {$is_eval eq 1} { - # eval sha is only relevant for is_eval Lua - test {EVALSHA - Can we call a SHA1 if already defined?} { - r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey - } {myval} - - test {EVALSHA_RO - Can we call a SHA1 if already defined?} { - r evalsha_ro fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey - } {myval} - - test {EVALSHA - Can we call a SHA1 in uppercase?} { - r evalsha FD758D1589D044DD850A6F05D52F2EEFD27F033F 1 mykey - } {myval} - - test {EVALSHA - Do we get an error on invalid SHA1?} { - catch {r evalsha NotValidShaSUM 0} e - set _ $e - } {NOSCRIPT*} - - test {EVALSHA - Do we get an error on non defined SHA1?} { - catch {r evalsha ffd632c7d33e571e9f24556ebed26c3479a87130 0} e - set _ $e - } {NOSCRIPT*} - } ;# is_eval - - test {EVAL - Redis integer -> Lua type conversion} { - r set x 0 - run_script { - local foo = redis.pcall('incr',KEYS[1]) - return {type(foo),foo} - } 1 x - } {number 1} - - test {EVAL - Lua number -> Redis integer conversion} { - r del hash - run_script { - local foo = redis.pcall('hincrby','hash','field',200000000) - return {type(foo),foo} - } 0 - } {number 200000000} - - test {EVAL - Redis bulk -> Lua type conversion} { - r set mykey myval - run_script { - local foo = redis.pcall('get',KEYS[1]) - return {type(foo),foo} - } 1 mykey - } {string myval} - - test {EVAL - Redis multi bulk -> Lua type conversion} { - r del mylist - r rpush mylist a - r rpush mylist b - r rpush mylist c - run_script { - local foo = redis.pcall('lrange',KEYS[1],0,-1) - return {type(foo),foo[1],foo[2],foo[3],# foo} - } 1 mylist - } {table a b c 3} - - test {EVAL - Redis status reply -> Lua type conversion} { - run_script { - local foo = redis.pcall('set',KEYS[1],'myval') - return {type(foo),foo['ok']} - } 1 mykey - } {table OK} - - test {EVAL - Redis error reply -> Lua type conversion} { - r set mykey myval - run_script { - local foo = redis.pcall('incr',KEYS[1]) - return {type(foo),foo['err']} - } 1 mykey - } {table {ERR value is not an integer or out of range}} - - test {EVAL - Redis nil bulk reply -> Lua type conversion} { - r del mykey - run_script { - local foo = redis.pcall('get',KEYS[1]) - return {type(foo),foo == false} - } 1 mykey - } {boolean 1} - - test {EVAL - Is the Lua client using the currently selected DB?} { - r set mykey "this is DB 9" - r select 10 - r set mykey "this is DB 10" - run_script {return redis.pcall('get',KEYS[1])} 1 mykey - } {this is DB 10} {singledb:skip} - - test {EVAL - SELECT inside Lua should not affect the caller} { - # here we DB 10 is selected - r set mykey "original value" - run_script {return redis.pcall('select','9')} 0 - set res [r get mykey] - r select 9 - set res - } {original value} {singledb:skip} - - if 0 { - test {EVAL - Script can't run more than configured time limit} { - r config set lua-time-limit 1 - catch { - run_script { - local i = 0 - while true do i=i+1 end - } 0 - } e - set _ $e - } {*execution time*} - } - - test {EVAL - Scripts do not block on blpop command} { - r lpush l 1 - r lpop l - run_script {return redis.pcall('blpop','l',0)} 1 l - } {} - - test {EVAL - Scripts do not block on brpop command} { - r lpush l 1 - r lpop l - run_script {return redis.pcall('brpop','l',0)} 1 l - } {} - - test {EVAL - Scripts do not block on brpoplpush command} { - r lpush empty_list1{t} 1 - r lpop empty_list1{t} - run_script {return redis.pcall('brpoplpush','empty_list1{t}', 'empty_list2{t}',0)} 2 empty_list1{t} empty_list2{t} - } {} - - test {EVAL - Scripts do not block on blmove command} { - r lpush empty_list1{t} 1 - r lpop empty_list1{t} - run_script {return redis.pcall('blmove','empty_list1{t}', 'empty_list2{t}', 'LEFT', 'LEFT', 0)} 2 empty_list1{t} empty_list2{t} - } {} - - test {EVAL - Scripts do not block on bzpopmin command} { - r zadd empty_zset 10 foo - r zmpop 1 empty_zset MIN - run_script {return redis.pcall('bzpopmin','empty_zset', 0)} 1 empty_zset - } {} - - test {EVAL - Scripts do not block on bzpopmax command} { - r zadd empty_zset 10 foo - r zmpop 1 empty_zset MIN - run_script {return redis.pcall('bzpopmax','empty_zset', 0)} 1 empty_zset - } {} - - test {EVAL - Scripts do not block on wait} { - run_script {return redis.pcall('wait','1','0')} 0 - } {0} - - test {EVAL - Scripts do not block on waitaof} { - r config set appendonly no - run_script {return redis.pcall('waitaof','0','1','0')} 0 - } {0 0} - - test {EVAL - Scripts do not block on XREAD with BLOCK option} { - r del s - r xgroup create s g $ MKSTREAM - set res [run_script {return redis.pcall('xread','STREAMS','s','$')} 1 s] - assert {$res eq {}} - run_script {return redis.pcall('xread','BLOCK',0,'STREAMS','s','$')} 1 s - } {} - - test {EVAL - Scripts do not block on XREADGROUP with BLOCK option} { - set res [run_script {return redis.pcall('xreadgroup','group','g','c','STREAMS','s','>')} 1 s] - assert {$res eq {}} - run_script {return redis.pcall('xreadgroup','group','g','c','BLOCK',0,'STREAMS','s','>')} 1 s - } {} - - test {EVAL - Scripts do not block on XREAD with BLOCK option -- non empty stream} { - r XADD s * a 1 - set res [run_script {return redis.pcall('xread','BLOCK',0,'STREAMS','s','$')} 1 s] - assert {$res eq {}} - - set res [run_script {return redis.pcall('xread','BLOCK',0,'STREAMS','s','0-0')} 1 s] - assert {[lrange [lindex $res 0 1 0 1] 0 1] eq {a 1}} - } - - test {EVAL - Scripts do not block on XREADGROUP with BLOCK option -- non empty stream} { - r XADD s * b 2 - set res [ - run_script {return redis.pcall('xreadgroup','group','g','c','BLOCK',0,'STREAMS','s','>')} 1 s - ] - assert {[llength [lindex $res 0 1]] == 2} - lindex $res 0 1 0 1 - } {a 1} - - test {EVAL - Scripts can run non-deterministic commands} { - set e {} - catch { - run_script {redis.pcall('randomkey'); return redis.pcall('set','x','ciao')} 1 x - } e - set e - } {*OK*} - - test {EVAL - No arguments to redis.call/pcall is considered an error} { - set e {} - catch {run_script {return redis.call()} 0} e - set e - } {*one argument*} - - test {EVAL - redis.call variant raises a Lua error on Redis cmd error (1)} { - set e {} - catch { - run_script "redis.call('nosuchcommand')" 0 - } e - set e - } {*Unknown Redis*} - - test {EVAL - redis.call variant raises a Lua error on Redis cmd error (1)} { - set e {} - catch { - run_script "redis.call('get','a','b','c')" 0 - } e - set e - } {*number of args*} - - test {EVAL - redis.call variant raises a Lua error on Redis cmd error (1)} { - set e {} - r set foo bar - catch { - run_script {redis.call('lpush',KEYS[1],'val')} 1 foo - } e - set e - } {*against a key*} - - test {EVAL - Test table unpack with invalid indexes} { - catch {run_script { return {unpack({1,2,3}, -2, 2147483647)} } 0} e - assert_match {*too many results to unpack*} $e - catch {run_script { return {unpack({1,2,3}, 0, 2147483647)} } 0} e - assert_match {*too many results to unpack*} $e - catch {run_script { return {unpack({1,2,3}, -2147483648, -2)} } 0} e - assert_match {*too many results to unpack*} $e - set res [run_script { return {unpack({1,2,3}, -1, -2)} } 0] - assert_match {} $res - set res [run_script { return {unpack({1,2,3}, 1, -1)} } 0] - assert_match {} $res - - # unpack with range -1 to 5, verify nil indexes - set res [run_script { - local function unpack_to_list(t, i, j) - local n, v = select('#', unpack(t, i, j)), {unpack(t, i, j)} - for i = 1, n do v[i] = v[i] or '_NIL_' end - v.n = n - return v - end - - return unpack_to_list({1,2,3}, -1, 5) - } 0] - assert_match {_NIL_ _NIL_ 1 2 3 _NIL_ _NIL_} $res - - # unpack with negative range, verify nil indexes - set res [run_script { - local function unpack_to_list(t, i, j) - local n, v = select('#', unpack(t, i, j)), {unpack(t, i, j)} - for i = 1, n do v[i] = v[i] or '_NIL_' end - v.n = n - return v - end - - return unpack_to_list({1,2,3}, -2147483648, -2147483646) - } 0] - assert_match {_NIL_ _NIL_ _NIL_} $res - } {} - - test {EVAL - JSON numeric decoding} { - # We must return the table as a string because otherwise - # Redis converts floats to ints and we get 0 and 1023 instead - # of 0.0003 and 1023.2 as the parsed output. - run_script {return - table.concat( - cjson.decode( - "[0.0, -5e3, -1, 0.3e-3, 1023.2, 0e10]"), " ") - } 0 - } {0 -5000 -1 0.0003 1023.2 0} - - test {EVAL - JSON string decoding} { - run_script {local decoded = cjson.decode('{"keya": "a", "keyb": "b"}') - return {decoded.keya, decoded.keyb} - } 0 - } {a b} - - test {EVAL - JSON empty array decoding} { - # Default behavior - assert_equal "{}" [run_script { - return cjson.encode(cjson.decode('[]')) - } 0] - assert_equal "{}" [run_script { - cjson.decode_array_with_array_mt(false) - return cjson.encode(cjson.decode('[]')) - } 0] - assert_equal "{\"item\":{}}" [run_script { - cjson.decode_array_with_array_mt(false) - return cjson.encode(cjson.decode('{"item": []}')) - } 0] - - # With array metatable - assert_equal "\[\]" [run_script { - cjson.decode_array_with_array_mt(true) - return cjson.encode(cjson.decode('[]')) - } 0] - assert_equal "{\"item\":\[\]}" [run_script { - cjson.decode_array_with_array_mt(true) - return cjson.encode(cjson.decode('{"item": []}')) - } 0] - } - - test {EVAL - JSON empty array decoding after element removal} { - # Default: emptied array becomes object - assert_equal "{}" [run_script { - cjson.decode_array_with_array_mt(false) - local t = cjson.decode('[1, 2]') - -- emptying the array - t[1] = nil - t[2] = nil - return cjson.encode(t) - } 0] - - # With array metatable: emptied array stays array - assert_equal "\[\]" [run_script { - cjson.decode_array_with_array_mt(true) - local t = cjson.decode('[1, 2]') - -- emptying the array - t[1] = nil - t[2] = nil - return cjson.encode(t) - } 0] - } - - test {EVAL - cjson array metatable modification should be readonly} { - catch { - run_script { - cjson.decode_array_with_array_mt(true) - local t = cjson.decode('[]') - getmetatable(t).__is_cjson_array = function() return 1 end - return cjson.encode(t) - } 0 - } e - set _ $e - } {*Attempt to modify a readonly table*} - - test {EVAL - JSON smoke test} { - run_script { - local some_map = { - s1="Some string", - n1=100, - a1={"Some","String","Array"}, - nil1=nil, - b1=true, - b2=false} - local encoded = cjson.encode(some_map) - local decoded = cjson.decode(encoded) - assert(table.concat(some_map) == table.concat(decoded)) - - cjson.encode_keep_buffer(false) - encoded = cjson.encode(some_map) - decoded = cjson.decode(encoded) - assert(table.concat(some_map) == table.concat(decoded)) - - -- Table with numeric keys - local table1 = {one="one", [1]="one"} - encoded = cjson.encode(table1) - decoded = cjson.decode(encoded) - assert(decoded["one"] == table1["one"]) - assert(decoded["1"] == table1[1]) - - -- Array - local array1 = {[1]="one", [2]="two"} - encoded = cjson.encode(array1) - decoded = cjson.decode(encoded) - assert(table.concat(array1) == table.concat(decoded)) - - -- Invalid keys - local invalid_map = {} - invalid_map[false] = "false" - local ok, encoded = pcall(cjson.encode, invalid_map) - assert(ok == false) - - -- Max depth - cjson.encode_max_depth(1) - ok, encoded = pcall(cjson.encode, some_map) - assert(ok == false) - - cjson.decode_max_depth(1) - ok, decoded = pcall(cjson.decode, '{"obj": {"array": [1,2,3,4]}}') - assert(ok == false) - - -- Invalid numbers - ok, encoded = pcall(cjson.encode, {num1=0/0}) - assert(ok == false) - cjson.encode_invalid_numbers(true) - ok, encoded = pcall(cjson.encode, {num1=0/0}) - assert(ok == true) - - -- Restore defaults - cjson.decode_max_depth(1000) - cjson.encode_max_depth(1000) - cjson.encode_invalid_numbers(false) - } 0 - } - - test {EVAL - cmsgpack can pack double?} { - run_script {local encoded = cmsgpack.pack(0.1) - local h = "" - for i = 1, #encoded do - h = h .. string.format("%02x",string.byte(encoded,i)) - end - return h - } 0 - } {cb3fb999999999999a} - - test {EVAL - cmsgpack can pack negative int64?} { - run_script {local encoded = cmsgpack.pack(-1099511627776) - local h = "" - for i = 1, #encoded do - h = h .. string.format("%02x",string.byte(encoded,i)) - end - return h - } 0 - } {d3ffffff0000000000} - - test {EVAL - cmsgpack pack/unpack smoke test} { - run_script { - local str_lt_32 = string.rep("x", 30) - local str_lt_255 = string.rep("x", 250) - local str_lt_65535 = string.rep("x", 65530) - local str_long = string.rep("x", 100000) - local array_lt_15 = {1, 2, 3, 4, 5} - local array_lt_65535 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18} - local array_big = {} - for i=1, 100000 do - array_big[i] = i - end - local map_lt_15 = {a=1, b=2} - local map_big = {} - for i=1, 100000 do - map_big[tostring(i)] = i - end - local some_map = { - s1=str_lt_32, - s2=str_lt_255, - s3=str_lt_65535, - s4=str_long, - d1=0.1, - i1=1, - i2=250, - i3=65530, - i4=100000, - i5=2^40, - i6=-1, - i7=-120, - i8=-32000, - i9=-100000, - i10=-3147483648, - a1=array_lt_15, - a2=array_lt_65535, - a3=array_big, - m1=map_lt_15, - m2=map_big, - b1=false, - b2=true, - n=nil - } - local encoded = cmsgpack.pack(some_map) - local decoded = cmsgpack.unpack(encoded) - assert(table.concat(some_map) == table.concat(decoded)) - local offset, decoded_one = cmsgpack.unpack_one(encoded, 0) - assert(table.concat(some_map) == table.concat(decoded_one)) - assert(offset == -1) - - local encoded_multiple = cmsgpack.pack(str_lt_32, str_lt_255, str_lt_65535, str_long) - local offset, obj = cmsgpack.unpack_limit(encoded_multiple, 1, 0) - assert(obj == str_lt_32) - offset, obj = cmsgpack.unpack_limit(encoded_multiple, 1, offset) - assert(obj == str_lt_255) - offset, obj = cmsgpack.unpack_limit(encoded_multiple, 1, offset) - assert(obj == str_lt_65535) - offset, obj = cmsgpack.unpack_limit(encoded_multiple, 1, offset) - assert(obj == str_long) - assert(offset == -1) - } 0 - } - - test {EVAL - cmsgpack can pack and unpack circular references?} { - run_script {local a = {x=nil,y=5} - local b = {x=a} - a['x'] = b - local encoded = cmsgpack.pack(a) - local h = "" - -- cmsgpack encodes to a depth of 16, but can't encode - -- references, so the encoded object has a deep copy recursive - -- depth of 16. - for i = 1, #encoded do - h = h .. string.format("%02x",string.byte(encoded,i)) - end - -- when unpacked, re.x.x != re because the unpack creates - -- individual tables down to a depth of 16. - -- (that's why the encoded output is so large) - local re = cmsgpack.unpack(encoded) - assert(re) - assert(re.x) - assert(re.x.x.y == re.y) - assert(re.x.x.x.x.y == re.y) - assert(re.x.x.x.x.x.x.y == re.y) - assert(re.x.x.x.x.x.x.x.x.x.x.y == re.y) - -- maximum working depth: - assert(re.x.x.x.x.x.x.x.x.x.x.x.x.x.x.y == re.y) - -- now the last x would be b above and has no y - assert(re.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x) - -- so, the final x.x is at the depth limit and was assigned nil - assert(re.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x == nil) - return {h, re.x.x.x.x.x.x.x.x.y == re.y, re.y == 5} - } 0 - } {82a17905a17881a17882a17905a17881a17882a17905a17881a17882a17905a17881a17882a17905a17881a17882a17905a17881a17882a17905a17881a17882a17905a17881a178c0 1 1} - - test {EVAL - Numerical sanity check from bitop} { - run_script {assert(0x7fffffff == 2147483647, "broken hex literals"); - assert(0xffffffff == -1 or 0xffffffff == 2^32-1, - "broken hex literals"); - assert(tostring(-1) == "-1", "broken tostring()"); - assert(tostring(0xffffffff) == "-1" or - tostring(0xffffffff) == "4294967295", - "broken tostring()") - } 0 - } {} - - test {EVAL - Verify minimal bitop functionality} { - run_script {assert(bit.tobit(1) == 1); - assert(bit.band(1) == 1); - assert(bit.bxor(1,2) == 3); - assert(bit.bor(1,2,4,8,16,32,64,128) == 255) - } 0 - } {} - - test {EVAL - Able to parse trailing comments} { - run_script {return 'hello' --trailing comment} 0 - } {hello} - - test {EVAL_RO - Successful case} { - r set foo bar - assert_equal bar [run_script_ro {return redis.call('get', KEYS[1]);} 1 foo] - } - - test {EVAL_RO - Cannot run write commands} { - r set foo bar - catch {run_script_ro {redis.call('del', KEYS[1]);} 1 foo} e - set e - } {ERR Write commands are not allowed from read-only scripts*} - - if {$is_eval eq 1} { - # script command is only relevant for is_eval Lua - test {SCRIPTING FLUSH - is able to clear the scripts cache?} { - r set mykey myval - - r script load {return redis.call('get',KEYS[1])} - set v [r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey] - assert_equal $v myval - r script flush - assert_error {NOSCRIPT*} {r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey} - - r eval {return redis.call('get',KEYS[1])} 1 mykey - set v [r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey] - assert_equal $v myval - r script flush - assert_error {NOSCRIPT*} {r evalsha fd758d1589d044dd850a6f05d52f2eefd27f033f 1 mykey} - } - - test {SCRIPTING FLUSH ASYNC} { - for {set j 0} {$j < 100} {incr j} { - r script load "return $j" - } - assert { [string match "*number_of_cached_scripts:100*" [r info Memory]] } - r script flush async - assert { [string match "*number_of_cached_scripts:0*" [r info Memory]] } - } - - test {SCRIPT EXISTS - can detect already defined scripts?} { - r eval "return 1+1" 0 - r script exists a27e7e8a43702b7046d4f6a7ccf5b60cef6b9bd9 a27e7e8a43702b7046d4f6a7ccf5b60cef6b9bda - } {1 0} - - test {SCRIPT LOAD - is able to register scripts in the scripting cache} { - list \ - [r script load "return 'loaded'"] \ - [r evalsha b534286061d4b9e4026607613b95c06c06015ae8 0] - } {b534286061d4b9e4026607613b95c06c06015ae8 loaded} - - test "SORT is normally not alpha re-ordered for the scripting engine" { - r del myset - r sadd myset 1 2 3 4 10 - r eval {return redis.call('sort',KEYS[1],'desc')} 1 myset - } {10 4 3 2 1} {cluster:skip} - - test "SORT BY <constant> output gets ordered for scripting" { - r del myset - r sadd myset a b c d e f g h i l m n o p q r s t u v z aa aaa azz - r eval {return redis.call('sort',KEYS[1],'by','_')} 1 myset - } {a aa aaa azz b c d e f g h i l m n o p q r s t u v z} {cluster:skip} - - test "SORT BY <constant> with GET gets ordered for scripting" { - r del myset - r sadd myset a b c - r eval {return redis.call('sort',KEYS[1],'by','_','get','#','get','_:*')} 1 myset - } {a {} b {} c {}} {cluster:skip} - } ;# is_eval - - test "redis.sha1hex() implementation" { - list [run_script {return redis.sha1hex('')} 0] \ - [run_script {return redis.sha1hex('Pizza & Mandolino')} 0] - } {da39a3ee5e6b4b0d3255bfef95601890afd80709 74822d82031af7493c20eefa13bd07ec4fada82f} - - test "Measures elapsed time os.clock()" { - set escaped [run_script { - local start = os.clock() - while os.clock() - start < 1 do end - return {double = os.clock() - start} - } 0] - assert_morethan_equal $escaped 1 ;# 1 second - } - - test "Prohibit dangerous lua methods in sandbox" { - assert_equal "" [run_script { - local allowed_methods = {"clock"} - -- Find a value from a tuple and return the position. - local indexOf = function(tuple, value) - for i, v in ipairs(tuple) do - if v == value then return i end - end - return nil - end - -- Check for disallowed methods and verify all allowed methods exist. - -- If an allowed method is found, it's removed from 'allowed_methods'. - -- If 'allowed_methods' is empty at the end, all allowed methods were found. - for key, value in pairs(os) do - local index = indexOf(allowed_methods, key) - if index == nil or type(value) ~= "function" then - return "Disallowed "..type(value)..":"..key - end - table.remove(allowed_methods, index) - end - if #allowed_methods ~= 0 then - return "Expected method not found: "..table.concat(allowed_methods, ",") - end - return "" - } 0] - } - - test "Verify execution of prohibit dangerous Lua methods will fail" { - assert_error {ERR *attempt to call field 'execute'*} {run_script {os.execute()} 0} - assert_error {ERR *attempt to call field 'exit'*} {run_script {os.exit()} 0} - assert_error {ERR *attempt to call field 'getenv'*} {run_script {os.getenv()} 0} - assert_error {ERR *attempt to call field 'remove'*} {run_script {os.remove()} 0} - assert_error {ERR *attempt to call field 'rename'*} {run_script {os.rename()} 0} - assert_error {ERR *attempt to call field 'setlocale'*} {run_script {os.setlocale()} 0} - assert_error {ERR *attempt to call field 'tmpname'*} {run_script {os.tmpname()} 0} - } - - test {Globals protection reading an undeclared global variable} { - catch {run_script {return a} 0} e - set e - } {ERR *attempted to access * global*} - - test {Globals protection setting an undeclared global*} { - catch {run_script {a=10} 0} e - set e - } {ERR *Attempt to modify a readonly table*} - - test {lua bit.tohex bug} { - set res [run_script {return bit.tohex(65535, -2147483648)} 0] - r ping - set res - } {0000FFFF} - - test {Test an example script DECR_IF_GT} { - set decr_if_gt { - local current - - current = redis.call('get',KEYS[1]) - if not current then return nil end - if current > ARGV[1] then - return redis.call('decr',KEYS[1]) - else - return redis.call('get',KEYS[1]) - end - } - r set foo 5 - set res {} - lappend res [run_script $decr_if_gt 1 foo 2] - lappend res [run_script $decr_if_gt 1 foo 2] - lappend res [run_script $decr_if_gt 1 foo 2] - lappend res [run_script $decr_if_gt 1 foo 2] - lappend res [run_script $decr_if_gt 1 foo 2] - set res - } {4 3 2 2 2} - - if {$is_eval eq 1} { - # random handling is only relevant for is_eval Lua - test {random numbers are random now} { - set rand1 [r eval {return tostring(math.random())} 0] - wait_for_condition 100 1 { - $rand1 ne [r eval {return tostring(math.random())} 0] - } else { - fail "random numbers should be random, now it's fixed value" - } - } - - test {Scripting engine PRNG can be seeded correctly} { - set rand1 [r eval { - math.randomseed(ARGV[1]); return tostring(math.random()) - } 0 10] - set rand2 [r eval { - math.randomseed(ARGV[1]); return tostring(math.random()) - } 0 10] - set rand3 [r eval { - math.randomseed(ARGV[1]); return tostring(math.random()) - } 0 20] - assert_equal $rand1 $rand2 - assert {$rand2 ne $rand3} - } - } ;# is_eval - - test {EVAL does not leak in the Lua stack} { - r script flush ;# reset Lua VM - r set x 0 - # Use a non blocking client to speedup the loop. - set rd [redis_deferring_client] - for {set j 0} {$j < 10000} {incr j} { - run_script_on_connection $rd {return redis.call("incr",KEYS[1])} 1 x - } - for {set j 0} {$j < 10000} {incr j} { - $rd read - } - assert {[s used_memory_lua] < 1024*100} - $rd close - r get x - } {10000} - - if {$is_eval eq 1} { - test {SPOP: We can call scripts rewriting client->argv from Lua} { - set repl [attach_to_replication_stream] - #this sadd operation is for external-cluster test. If myset doesn't exist, 'del myset' won't get propagated. - r sadd myset ppp - r del myset - r sadd myset a b c - assert {[r eval {return redis.call('spop', 'myset')} 0] ne {}} - assert {[r eval {return redis.call('spop', 'myset', 1)} 0] ne {}} - assert {[r eval {return redis.call('spop', KEYS[1])} 1 myset] ne {}} - # this one below should not be replicated - assert {[r eval {return redis.call('spop', KEYS[1])} 1 myset] eq {}} - r set trailingkey 1 - assert_replication_stream $repl { - {select *} - {sadd *} - {del *} - {sadd *} - {srem myset *} - {srem myset *} - {srem myset *} - {set *} - } - close_replication_stream $repl - } {} {needs:repl} - - test {MGET: mget shouldn't be propagated in Lua} { - set repl [attach_to_replication_stream] - r mset a{t} 1 b{t} 2 c{t} 3 d{t} 4 - #read-only, won't be replicated - assert {[r eval {return redis.call('mget', 'a{t}', 'b{t}', 'c{t}', 'd{t}')} 0] eq {1 2 3 4}} - r set trailingkey 2 - assert_replication_stream $repl { - {select *} - {mset *} - {set *} - } - close_replication_stream $repl - } {} {needs:repl} - - test {EXPIRE: We can call scripts rewriting client->argv from Lua} { - set repl [attach_to_replication_stream] - r set expirekey 1 - #should be replicated as EXPIREAT - assert {[r eval {return redis.call('expire', KEYS[1], ARGV[1])} 1 expirekey 3] eq 1} - - assert_replication_stream $repl { - {select *} - {set *} - {pexpireat expirekey *} - } - close_replication_stream $repl - } {} {needs:repl} - - test {INCRBYFLOAT: We can call scripts expanding client->argv from Lua} { - # coverage for scripts calling commands that expand the argv array - # an attempt to add coverage for a possible bug in luaArgsToRedisArgv - # this test needs a fresh server so that lua_argv_size is 0. - # glibc realloc can return the same pointer even when the size changes - # still this test isn't able to trigger the issue, but we keep it anyway. - start_server {tags {"scripting"}} { - set repl [attach_to_replication_stream] - # a command with 5 argsument - r eval {redis.call('hmget', KEYS[1], 1, 2, 3)} 1 key - # then a command with 3 that is replicated as one with 4 - r eval {redis.call('incrbyfloat', KEYS[1], 1)} 1 key - # then a command with 4 args - r eval {redis.call('set', KEYS[1], '1', 'KEEPTTL')} 1 key - - assert_replication_stream $repl { - {select *} - {set key 1 KEEPTTL} - {set key 1 KEEPTTL} - } - close_replication_stream $repl - } - } {} {needs:repl} - - } ;# is_eval - - test {Call Redis command with many args from Lua (issue #1764)} { - run_script { - local i - local x={} - redis.call('del','mylist') - for i=1,100 do - table.insert(x,i) - end - redis.call('rpush','mylist',unpack(x)) - return redis.call('lrange','mylist',0,-1) - } 1 mylist - } {1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100} - - test {Number conversion precision test (issue #1118)} { - run_script { - local value = 9007199254740991 - redis.call("set","foo",value) - return redis.call("get","foo") - } 1 foo - } {9007199254740991} - - test {String containing number precision test (regression of issue #1118)} { - run_script { - redis.call("set", "key", "12039611435714932082") - return redis.call("get", "key") - } 1 key - } {12039611435714932082} - - test {Verify negative arg count is error instead of crash (issue #1842)} { - catch { run_script { return "hello" } -12 } e - set e - } {ERR Number of keys can't be negative} - - test {Scripts can handle commands with incorrect arity} { - assert_error "ERR Wrong number of args calling Redis command from script*" {run_script "redis.call('set','invalid')" 0} - assert_error "ERR Wrong number of args calling Redis command from script*" {run_script "redis.call('incr')" 0} - } - - test {Correct handling of reused argv (issue #1939)} { - run_script { - for i = 0, 10 do - redis.call('SET', 'a{t}', '1') - redis.call('MGET', 'a{t}', 'b{t}', 'c{t}') - redis.call('EXPIRE', 'a{t}', 0) - redis.call('GET', 'a{t}') - redis.call('MGET', 'a{t}', 'b{t}', 'c{t}') - end - } 3 a{t} b{t} c{t} - } - - test {Functions in the Redis namespace are able to report errors} { - catch { - run_script { - redis.sha1hex() - } 0 - } e - set e - } {*wrong number*} - - test {CLUSTER RESET can not be invoke from within a script} { - catch { - run_script { - redis.call('cluster', 'reset', 'hard') - } 0 - } e - set _ $e - } {*command is not allowed*} - - test {Script with RESP3 map} { - set expected_dict [dict create field value] - set expected_list [list field value] - - # Sanity test for RESP3 without scripts - r HELLO 3 - r hset hash field value - set res [r hgetall hash] - assert_equal $res $expected_dict - - # Test RESP3 client with script in both RESP2 and RESP3 modes - set res [run_script {redis.setresp(3); return redis.call('hgetall', KEYS[1])} 1 hash] - assert_equal $res $expected_dict - set res [run_script {redis.setresp(2); return redis.call('hgetall', KEYS[1])} 1 hash] - assert_equal $res $expected_list - - # Test RESP2 client with script in both RESP2 and RESP3 modes - r HELLO 2 - set res [run_script {redis.setresp(3); return redis.call('hgetall', KEYS[1])} 1 hash] - assert_equal $res $expected_list - set res [run_script {redis.setresp(2); return redis.call('hgetall', KEYS[1])} 1 hash] - assert_equal $res $expected_list - } {} {resp3} - - if {!$::log_req_res} { # this test creates a huge nested array which python can't handle (RecursionError: maximum recursion depth exceeded in comparison) - test {Script return recursive object} { - r readraw 1 - set res [run_script {local a = {}; local b = {a}; a[1] = b; return a} 0] - # drain the response - while {true} { - if {$res == "-ERR reached lua stack limit"} { - break - } - assert_equal $res "*1" - set res [r read] - } - r readraw 0 - # make sure the connection is still valid - assert_equal [r ping] {PONG} - } - } - - test {Script check unpack with massive arguments} { - run_script { - local a = {} - for i=1,7999 do - a[i] = 1 - end - return redis.call("lpush", "l", unpack(a)) - } 1 l - } {7999} - - test "Script read key with expiration set" { - r SET key value EX 10 - assert_equal [run_script { - if redis.call("EXISTS", "key") then - return redis.call("GET", "key") - else - return redis.call("EXISTS", "key") - end - } 1 key] "value" - } - - test "Script del key with expiration set" { - r SET key value EX 10 - assert_equal [run_script { - redis.call("DEL", "key") - return redis.call("EXISTS", "key") - } 1 key] 0 - } - - test "Script ACL check" { - r acl setuser bob on {>123} {+@scripting} {+set} {~x*} - assert_equal [r auth bob 123] {OK} - - # Check permission granted - assert_equal [run_script { - return redis.acl_check_cmd('set','xx',1) - } 1 xx] 1 - - # Check permission denied unauthorised command - assert_equal [run_script { - return redis.acl_check_cmd('hset','xx','f',1) - } 1 xx] {} - - # Check permission denied unauthorised key - # Note: we don't pass the "yy" key as an argument to the script so key acl checks won't block the script - assert_equal [run_script { - return redis.acl_check_cmd('set','yy',1) - } 0] {} - - # Check error due to invalid command - assert_error {ERR *Invalid command passed to redis.acl_check_cmd()*} {run_script { - return redis.acl_check_cmd('invalid-cmd','arg') - } 0} - } - - test "Binary code loading failed" { - assert_error {ERR *attempt to call a nil value*} {run_script { - return loadstring(string.dump(function() return 1 end))() - } 0} - } - - test "Try trick global protection 1" { - catch { - run_script { - setmetatable(_G, {}) - } 0 - } e - set _ $e - } {*Attempt to modify a readonly table*} - - test "Try trick global protection 2" { - catch { - run_script { - local g = getmetatable(_G) - g.__index = {} - } 0 - } e - set _ $e - } {*Attempt to modify a readonly table*} - - test "Try trick global protection 3" { - catch { - run_script { - redis = function() return 1 end - } 0 - } e - set _ $e - } {*Attempt to modify a readonly table*} - - test "Try trick global protection 4" { - catch { - run_script { - _G = {} - } 0 - } e - set _ $e - } {*Attempt to modify a readonly table*} - - test "Try trick readonly table on redis table" { - catch { - run_script { - redis.call = function() return 1 end - } 0 - } e - set _ $e - } {*Attempt to modify a readonly table*} - - test "Try trick readonly table on json table" { - catch { - run_script { - cjson.encode = function() return 1 end - } 0 - } e - set _ $e - } {*Attempt to modify a readonly table*} - - test "Try trick readonly table on cmsgpack table" { - catch { - run_script { - cmsgpack.pack = function() return 1 end - } 0 - } e - set _ $e - } {*Attempt to modify a readonly table*} - - test "Try trick readonly table on bit table" { - catch { - run_script { - bit.lshift = function() return 1 end - } 0 - } e - set _ $e - } {*Attempt to modify a readonly table*} - - test "Try trick readonly table on basic types metatable" { - # Run the following scripts for basic types. Either getmetatable() - # should return nil or the metatable must be readonly. - set scripts { - {getmetatable(nil).__index = function() return 1 end} - {getmetatable('').__index = function() return 1 end} - {getmetatable(123.222).__index = function() return 1 end} - {getmetatable(true).__index = function() return 1 end} - {getmetatable(function() return 1 end).__index = function() return 1 end} - {getmetatable(coroutine.create(function() return 1 end)).__index = function() return 1 end} - } - - foreach code $scripts { - catch {run_script $code 0} e - assert { - [string match "*attempt to index a nil value script*" $e] || - [string match "*Attempt to modify a readonly table*" $e] - } - } - } - - test "Test loadfile are not available" { - catch { - run_script { - loadfile('some file') - } 0 - } e - set _ $e - } {*Script attempted to access nonexistent global variable 'loadfile'*} - - test "Test dofile are not available" { - catch { - run_script { - dofile('some file') - } 0 - } e - set _ $e - } {*Script attempted to access nonexistent global variable 'dofile'*} - - test "Test print are not available" { - catch { - run_script { - print('some data') - } 0 - } e - set _ $e - } {*Script attempted to access nonexistent global variable 'print'*} -} - -# start a new server to test the large-memory tests -start_server {tags {"scripting external:skip large-memory"}} { - test {EVAL - JSON string encoding a string larger than 2GB} { - run_script { - local s = string.rep("a", 1024 * 1024 * 1024) - return #cjson.encode(s..s..s) - } 0 - } {3221225474} ;# length includes two double quotes at both ends - - test {EVAL - Test long escape sequences for strings} { - run_script { - -- Generate 1gb '==...==' separator - local s = string.rep('=', 1024 * 1024) - local t = {} for i=1,1024 do t[i] = s end - local sep = table.concat(t) - collectgarbage('collect') - - local code = table.concat({'return [',sep,'[x]',sep,']'}) - collectgarbage('collect') - - -- Load the code and run it. Script will return the string length. - -- Escape sequence: [=....=[ to ]=...=] will be ignored - -- Actual string is a single character: 'x'. Script will return 1 - local func = loadstring(code) - return #func() - } 0 - } {1} - - test {EVAL - Lua can parse string with too many new lines} { - # Create a long string consisting only of newline characters. When Lua - # fails to parse a string, it typically includes a snippet like - # "... near ..." in the error message to indicate the last recognizable - # token. In this test, since the input contains only newlines, there - # should be no identifiable token, so the error message should contain - # only the actual error, without a near clause. - - run_script { - local s = string.rep('\n', 1024 * 1024) - local t = {} for i=1,2048 do t[#t+1] = s end - local lines = table.concat(t) - local fn, err = loadstring(lines) - return err - } 0 - } {*chunk has too many lines} -} - -# Start a new server to test lua-enable-deprecated-api config -foreach enabled {no yes} { -start_server [subst {tags {"scripting external:skip"} overrides {lua-enable-deprecated-api $enabled}}] { - test "Test setfenv availability lua-enable-deprecated-api=$enabled" { - catch { - run_script { - local f = function() return 1 end - setfenv(f, {}) - return 0 - } 0 - } e - if {$enabled} { - assert_equal $e 0 - } else { - assert_match {*Script attempted to access nonexistent global variable 'setfenv'*} $e - } - } - - test "Test getfenv availability lua-enable-deprecated-api=$enabled" { - catch { - run_script { - local f = function() return 1 end - getfenv(f) - return 0 - } 0 - } e - if {$enabled} { - assert_equal $e 0 - } else { - assert_match {*Script attempted to access nonexistent global variable 'getfenv'*} $e - } - } - - test "Test newproxy availability lua-enable-deprecated-api=$enabled" { - catch { - run_script { - getmetatable(newproxy(true)).__gc = function() return 1 end - return 0 - } 0 - } e - if {$enabled} { - assert_equal $e 0 - } else { - assert_match {*Script attempted to access nonexistent global variable 'newproxy'*} $e - } - } -} -} - -# Start a new server since the last test in this stanza will kill the -# instance at all. -start_server {tags {"scripting"}} { - test {Timedout read-only scripts can be killed by SCRIPT KILL} { - set rd [redis_deferring_client] - r config set lua-time-limit 10 - run_script_on_connection $rd {while true do end} 0 - after 200 - catch {r ping} e - assert_match {BUSY*} $e - kill_script - after 200 ; # Give some time to Lua to call the hook again... - assert_equal [r ping] "PONG" - $rd close - } - - test {Timedout read-only scripts can be killed by SCRIPT KILL even when use pcall} { - set rd [redis_deferring_client] - r config set lua-time-limit 10 - run_script_on_connection $rd {local f = function() while 1 do redis.call('ping') end end while 1 do pcall(f) end} 0 - - wait_for_condition 50 100 { - [catch {r ping} e] == 1 - } else { - fail "Can't wait for script to start running" - } - catch {r ping} e - assert_match {BUSY*} $e - - kill_script - - wait_for_condition 50 100 { - [catch {r ping} e] == 0 - } else { - fail "Can't wait for script to be killed" - } - assert_equal [r ping] "PONG" - - catch {$rd read} res - $rd close - - assert_match {*killed by user*} $res - } - - test {Timedout script does not cause a false dead client} { - set rd [redis_deferring_client] - r config set lua-time-limit 10 - - # senging (in a pipeline): - # 1. eval "while 1 do redis.call('ping') end" 0 - # 2. ping - if {$is_eval == 1} { - set buf "*3\r\n\$4\r\neval\r\n\$33\r\nwhile 1 do redis.call('ping') end\r\n\$1\r\n0\r\n" - append buf "*1\r\n\$4\r\nping\r\n" - } else { - set buf "*4\r\n\$8\r\nfunction\r\n\$4\r\nload\r\n\$7\r\nreplace\r\n\$97\r\n#!lua name=test\nredis.register_function('test', function() while 1 do redis.call('ping') end end)\r\n" - append buf "*3\r\n\$5\r\nfcall\r\n\$4\r\ntest\r\n\$1\r\n0\r\n" - append buf "*1\r\n\$4\r\nping\r\n" - } - $rd write $buf - $rd flush - - wait_for_condition 50 100 { - [catch {r ping} e] == 1 - } else { - fail "Can't wait for script to start running" - } - catch {r ping} e - assert_match {BUSY*} $e - - kill_script - wait_for_condition 50 100 { - [catch {r ping} e] == 0 - } else { - fail "Can't wait for script to be killed" - } - assert_equal [r ping] "PONG" - - if {$is_eval == 0} { - # read the function name - assert_match {test} [$rd read] - } - - catch {$rd read} res - assert_match {*killed by user*} $res - - set res [$rd read] - assert_match {*PONG*} $res - - $rd close - } - - test {Timedout script link is still usable after Lua returns} { - r config set lua-time-limit 10 - run_script {for i=1,100000 do redis.call('ping') end return 'ok'} 0 - r ping - } {PONG} - - test {Timedout scripts and unblocked command} { - # make sure a command that's allowed during BUSY doesn't trigger an unblocked command - - # enable AOF to also expose an assertion if the bug would happen - r flushall - r config set appendonly yes - - # create clients, and set one to block waiting for key 'x' - set rd [redis_deferring_client] - set rd2 [redis_deferring_client] - set r3 [redis_client] - $rd2 blpop x 0 - wait_for_blocked_clients_count 1 - - # hack: allow the script to use client list command so that we can control when it aborts - r DEBUG set-disable-deny-scripts 1 - r config set lua-time-limit 10 - run_script_on_connection $rd { - local clients - redis.call('lpush',KEYS[1],'y'); - while true do - clients = redis.call('client','list') - if string.find(clients, 'abortscript') ~= nil then break end - end - redis.call('lpush',KEYS[1],'z'); - return clients - } 1 x - - # wait for the script to be busy - after 200 - catch {r ping} e - assert_match {BUSY*} $e - - # run cause the script to abort, and run a command that could have processed - # unblocked clients (due to a bug) - $r3 hello 2 setname abortscript - - # make sure the script completed before the pop was processed - assert_equal [$rd2 read] {x z} - assert_match {*abortscript*} [$rd read] - - $rd close - $rd2 close - $r3 close - r DEBUG set-disable-deny-scripts 0 - } {OK} {external:skip needs:debug} - - test {Timedout scripts that modified data can't be killed by SCRIPT KILL} { - set rd [redis_deferring_client] - r config set lua-time-limit 10 - run_script_on_connection $rd {redis.call('set',KEYS[1],'y'); while true do end} 1 x - after 200 - catch {r ping} e - assert_match {BUSY*} $e - catch {kill_script} e - assert_match {UNKILLABLE*} $e - catch {r ping} e - assert_match {BUSY*} $e - } {} {external:skip} - - # Note: keep this test at the end of this server stanza because it - # kills the server. - test {SHUTDOWN NOSAVE can kill a timedout script anyway} { - # The server should be still unresponding to normal commands. - catch {r ping} e - assert_match {BUSY*} $e - catch {r shutdown nosave} - # Make sure the server was killed - catch {set rd [redis_deferring_client]} e - assert_match {*connection refused*} $e - } {} {external:skip} -} - - start_server {tags {"scripting repl needs:debug external:skip"}} { - start_server {} { - test "Before the replica connects we issue two EVAL commands" { - # One with an error, but still executing a command. - # SHA is: 67164fc43fa971f76fd1aaeeaf60c1c178d25876 - catch { - run_script {redis.call('incr',KEYS[1]); redis.call('nonexisting')} 1 x - } - # One command is correct: - # SHA is: 6f5ade10a69975e903c6d07b10ea44c6382381a5 - run_script {return redis.call('incr',KEYS[1])} 1 x - } {2} - - test "Connect a replica to the master instance" { - r -1 slaveof [srv 0 host] [srv 0 port] - wait_for_condition 50 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" - } - } - - if {$is_eval eq 1} { - test "Now use EVALSHA against the master, with both SHAs" { - # The server should replicate successful and unsuccessful - # commands as EVAL instead of EVALSHA. - catch { - r evalsha 67164fc43fa971f76fd1aaeeaf60c1c178d25876 1 x - } - r evalsha 6f5ade10a69975e903c6d07b10ea44c6382381a5 1 x - } {4} - - test "'x' should be '4' for EVALSHA being replicated by effects" { - wait_for_condition 50 100 { - [r -1 get x] eq {4} - } else { - fail "Expected 4 in x, but value is '[r -1 get x]'" - } - } - } ;# is_eval - - test "Replication of script multiple pushes to list with BLPOP" { - set rd [redis_deferring_client] - $rd brpop a 0 - run_script { - redis.call("lpush",KEYS[1],"1"); - redis.call("lpush",KEYS[1],"2"); - } 1 a - set res [$rd read] - $rd close - wait_for_condition 50 100 { - [r -1 lrange a 0 -1] eq [r lrange a 0 -1] - } else { - fail "Expected list 'a' in replica and master to be the same, but they are respectively '[r -1 lrange a 0 -1]' and '[r lrange a 0 -1]'" - } - set res - } {a 1} - - if {$is_eval eq 1} { - test "EVALSHA replication when first call is readonly" { - r del x - r eval {if tonumber(ARGV[1]) > 0 then redis.call('incr', KEYS[1]) end} 1 x 0 - r evalsha 6e0e2745aa546d0b50b801a20983b70710aef3ce 1 x 0 - r evalsha 6e0e2745aa546d0b50b801a20983b70710aef3ce 1 x 1 - wait_for_condition 50 100 { - [r -1 get x] eq {1} - } else { - fail "Expected 1 in x, but value is '[r -1 get x]'" - } - } - } ;# is_eval - - test "Lua scripts using SELECT are replicated correctly" { - run_script { - redis.call("set","foo1","bar1") - redis.call("select","10") - redis.call("incr","x") - redis.call("select","11") - redis.call("incr","z") - } 3 foo1 x z - run_script { - redis.call("set","foo1","bar1") - redis.call("select","10") - redis.call("incr","x") - redis.call("select","11") - redis.call("incr","z") - } 3 foo1 x z - wait_for_condition 50 100 { - [debug_digest -1] eq [debug_digest] - } else { - fail "Master-Replica desync after Lua script using SELECT." - } - } {} {singledb:skip} - } - } - -start_server {tags {"scripting repl external:skip"}} { - start_server {overrides {appendonly yes aof-use-rdb-preamble no}} { - test "Connect a replica to the master instance" { - r -1 slaveof [srv 0 host] [srv 0 port] - wait_for_condition 50 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" - } - } - - # replicate_commands is the default on Redis Function - test "Redis.replicate_commands() can be issued anywhere now" { - r eval { - redis.call('set','foo','bar'); - return redis.replicate_commands(); - } 0 - } {1} - - test "Redis.set_repl() can be issued before replicate_commands() now" { - catch { - r eval { - redis.set_repl(redis.REPL_ALL); - } 0 - } e - set e - } {} - - test "Redis.set_repl() don't accept invalid values" { - catch { - run_script { - redis.set_repl(12345); - } 0 - } e - set e - } {*Invalid*flags*} - - test "Test selective replication of certain Redis commands from Lua" { - r del a b c d - run_script { - redis.call('set','a','1'); - redis.set_repl(redis.REPL_NONE); - redis.call('set','b','2'); - redis.set_repl(redis.REPL_AOF); - redis.call('set','c','3'); - redis.set_repl(redis.REPL_ALL); - redis.call('set','d','4'); - } 4 a b c d - - wait_for_condition 50 100 { - [r -1 mget a b c d] eq {1 {} {} 4} - } else { - fail "Only a and d should be replicated to replica" - } - - # Master should have everything right now - assert {[r mget a b c d] eq {1 2 3 4}} - - # After an AOF reload only a, c and d should exist - r debug loadaof - - assert {[r mget a b c d] eq {1 {} 3 4}} - } - - test "PRNG is seeded randomly for command replication" { - if {$is_eval eq 1} { - # on is_eval Lua we need to call redis.replicate_commands() to get real randomization - set a [ - run_script { - redis.replicate_commands() - return math.random()*100000; - } 0 - ] - set b [ - run_script { - redis.replicate_commands() - return math.random()*100000; - } 0 - ] - } else { - set a [ - run_script { - return math.random()*100000; - } 0 - ] - set b [ - run_script { - return math.random()*100000; - } 0 - ] - } - assert {$a ne $b} - } - - test "Using side effects is not a problem with command replication" { - run_script { - redis.call('set','time',redis.call('time')[1]) - } 0 - - assert {[r get time] ne {}} - - wait_for_condition 50 100 { - [r get time] eq [r -1 get time] - } else { - fail "Time key does not match between master and replica" - } - } - } -} - -if {$is_eval eq 1} { -start_server {tags {"scripting external:skip"}} { - r script debug sync - r eval {return 'hello'} 0 - r eval {return 'hello'} 0 -} - -start_server {tags {"scripting needs:debug external:skip"}} { - test {Test scripting debug protocol parsing} { - r script debug sync - r eval {return 'hello'} 0 - catch {r 'hello\0world'} e - assert_match {*Unknown Redis Lua debugger command*} $e - catch {r 'hello\0'} e - assert_match {*Unknown Redis Lua debugger command*} $e - catch {r '\0hello'} e - assert_match {*Unknown Redis Lua debugger command*} $e - catch {r '\0hello\0'} e - assert_match {*Unknown Redis Lua debugger command*} $e - } - - test {Test scripting debug lua stack overflow} { - r script debug sync - r eval {return 'hello'} 0 - set cmd "*101\r\n\$5\r\nredis\r\n" - append cmd [string repeat "\$4\r\ntest\r\n" 100] - r write $cmd - r flush - set ret [r read] - assert_match {*Unknown Redis command called from script*} $ret - # make sure the server is still ok - reconnect - assert_equal [r ping] {PONG} - } -} - -start_server {tags {"scripting external:skip"}} { - test {Lua scripts eviction does not generate many scripts} { - r script flush - r config resetstat - - # "return 1" sha is: e0e1f9fabfc9d4800c877a703b823ac0578ff8db - # "return 500" sha is: 98fe65896b61b785c5ed328a5a0a1421f4f1490c - for {set j 1} {$j <= 250} {incr j} { - r eval "return $j" 0 - } - for {set j 251} {$j <= 500} {incr j} { - r eval_ro "return $j" 0 - } - assert_equal [s number_of_cached_scripts] 500 - assert_equal 1 [r evalsha e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0] - assert_equal 1 [r evalsha_ro e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0] - assert_equal 500 [r evalsha 98fe65896b61b785c5ed328a5a0a1421f4f1490c 0] - assert_equal 500 [r evalsha_ro 98fe65896b61b785c5ed328a5a0a1421f4f1490c 0] - - # Scripts between "return 1" and "return 500" are evicted - for {set j 501} {$j <= 750} {incr j} { - r eval "return $j" 0 - } - for {set j 751} {$j <= 1000} {incr j} { - r eval "return $j" 0 - } - assert_error {NOSCRIPT*} {r evalsha e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0} - assert_error {NOSCRIPT*} {r evalsha_ro e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0} - assert_error {NOSCRIPT*} {r evalsha 98fe65896b61b785c5ed328a5a0a1421f4f1490c 0} - assert_error {NOSCRIPT*} {r evalsha_ro 98fe65896b61b785c5ed328a5a0a1421f4f1490c 0} - - assert_equal [s evicted_scripts] 500 - assert_equal [s number_of_cached_scripts] 500 - } - - test {Lua scripts eviction is plain LRU} { - r script flush - r config resetstat - - # "return 1" sha is: e0e1f9fabfc9d4800c877a703b823ac0578ff8db - # "return 2" sha is: 7f923f79fe76194c868d7e1d0820de36700eb649 - # "return 3" sha is: 09d3822de862f46d784e6a36848b4f0736dda47a - # "return 500" sha is: 98fe65896b61b785c5ed328a5a0a1421f4f1490c - # "return 1000" sha is: 94f1a7bc9f985a1a1d5a826a85579137d9d840c8 - for {set j 1} {$j <= 500} {incr j} { - r eval "return $j" 0 - } - - # Call "return 1" to move it to the tail. - r eval "return 1" 0 - # Call "return 2" to move it to the tail. - r evalsha 7f923f79fe76194c868d7e1d0820de36700eb649 0 - # Create a new script, "return 3" will be evicted. - r eval "return 1000" 0 - # "return 1" is ok since it was moved to tail. - assert_equal 1 [r evalsha e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0] - # "return 2" is ok since it was moved to tail. - assert_equal 1 [r evalsha e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0] - # "return 3" was evicted. - assert_error {NOSCRIPT*} {r evalsha 09d3822de862f46d784e6a36848b4f0736dda47a 0} - # Others are ok. - assert_equal 500 [r evalsha 98fe65896b61b785c5ed328a5a0a1421f4f1490c 0] - assert_equal 1000 [r evalsha 94f1a7bc9f985a1a1d5a826a85579137d9d840c8 0] - - assert_equal [s evicted_scripts] 1 - assert_equal [s number_of_cached_scripts] 500 - } - - test {Lua scripts eviction does not affect script load} { - r script flush - r config resetstat - - set num [randomRange 500 1000] - for {set j 1} {$j <= $num} {incr j} { - r script load "return $j" - r eval "return 'str_$j'" 0 - } - set evicted [s evicted_scripts] - set cached [s number_of_cached_scripts] - # evicted = num eval scripts - 500 eval scripts - assert_equal $evicted [expr $num-500] - # cached = num load scripts + 500 eval scripts - assert_equal $cached [expr $num+500] - } -} - -} ;# is_eval - -start_server {tags {"scripting needs:debug"}} { - r debug set-disable-deny-scripts 1 - - for {set i 2} {$i <= 3} {incr i} { - for {set client_proto 2} {$client_proto <= 3} {incr client_proto} { - if {[lsearch $::denytags "resp3"] >= 0} { - if {$client_proto == 3} {continue} - } elseif {$::force_resp3} { - if {$client_proto == 2} {continue} - } - r hello $client_proto - set extra "RESP$i/$client_proto" - r readraw 1 - - test "test $extra big number protocol parsing" { - set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'bignum')" 0] - if {$client_proto == 2 || $i == 2} { - # if either Lua or the client is RESP2 the reply will be RESP2 - assert_equal $ret {$37} - assert_equal [r read] {1234567999999999999999999999999999999} - } else { - assert_equal $ret {(1234567999999999999999999999999999999} - } - } - - test "test $extra malformed big number protocol parsing" { - set ret [run_script "return {big_number='123\\r\\n123'}" 0] - if {$client_proto == 2} { - # if either Lua or the client is RESP2 the reply will be RESP2 - assert_equal $ret {$8} - assert_equal [r read] {123 123} - } else { - assert_equal $ret {(123 123} - } - } - - test "test $extra map protocol parsing" { - set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'map')" 0] - if {$client_proto == 2 || $i == 2} { - # if either Lua or the client is RESP2 the reply will be RESP2 - assert_equal $ret {*6} - } else { - assert_equal $ret {%3} - } - for {set j 0} {$j < 6} {incr j} { - r read - } - } - - test "test $extra set protocol parsing" { - set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'set')" 0] - if {$client_proto == 2 || $i == 2} { - # if either Lua or the client is RESP2 the reply will be RESP2 - assert_equal $ret {*3} - } else { - assert_equal $ret {~3} - } - for {set j 0} {$j < 3} {incr j} { - r read - } - } - - test "test $extra double protocol parsing" { - set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'double')" 0] - if {$client_proto == 2 || $i == 2} { - # if either Lua or the client is RESP2 the reply will be RESP2 - assert_equal $ret {$5} - assert_equal [r read] {3.141} - } else { - assert_equal $ret {,3.141} - } - } - - test "test $extra null protocol parsing" { - set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'null')" 0] - if {$client_proto == 2} { - # null is a special case in which a Lua client format does not effect the reply to the client - assert_equal $ret {$-1} - } else { - assert_equal $ret {_} - } - } {} - - test "test $extra verbatim protocol parsing" { - set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'verbatim')" 0] - if {$client_proto == 2 || $i == 2} { - # if either Lua or the client is RESP2 the reply will be RESP2 - assert_equal $ret {$25} - assert_equal [r read] {This is a verbatim} - assert_equal [r read] {string} - } else { - assert_equal $ret {=29} - assert_equal [r read] {txt:This is a verbatim} - assert_equal [r read] {string} - } - } - - test "test $extra true protocol parsing" { - set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'true')" 0] - if {$client_proto == 2 || $i == 2} { - # if either Lua or the client is RESP2 the reply will be RESP2 - assert_equal $ret {:1} - } else { - assert_equal $ret {#t} - } - } - - test "test $extra false protocol parsing" { - set ret [run_script "redis.setresp($i);return redis.call('debug', 'protocol', 'false')" 0] - if {$client_proto == 2 || $i == 2} { - # if either Lua or the client is RESP2 the reply will be RESP2 - assert_equal $ret {:0} - } else { - assert_equal $ret {#f} - } - } - - r readraw 0 - r hello 2 - } - } - - # attribute is not relevant to test with resp2 - test {test resp3 attribute protocol parsing} { - # attributes are not (yet) expose to the script - # So here we just check the parser handles them and they are ignored. - run_script "redis.setresp(3);return redis.call('debug', 'protocol', 'attrib')" 0 - } {Some real reply following the attribute} - - test "Script block the time during execution" { - assert_equal [run_script { - redis.call("SET", "key", "value", "PX", "1") - redis.call("DEBUG", "SLEEP", 0.01) - return redis.call("EXISTS", "key") - } 1 key] 1 - - assert_equal 0 [r EXISTS key] - } - - test "Script delete the expired key" { - r DEBUG set-active-expire 0 - r SET key value PX 1 - after 2 - - # use DEBUG OBJECT to make sure it doesn't error (means the key still exists) - r DEBUG OBJECT key - - assert_equal [run_script {return redis.call('EXISTS', 'key')} 1 key] 0 - assert_equal 0 [r EXISTS key] - r DEBUG set-active-expire 1 - } - - test "TIME command using cached time" { - set res [run_script { - local result1 = {redis.call("TIME")} - redis.call("DEBUG", "SLEEP", 0.01) - local result2 = {redis.call("TIME")} - return {result1, result2} - } 0] - assert_equal [lindex $res 0] [lindex $res 1] - } - - test "Script block the time in some expiration related commands" { - # The test uses different commands to set the "same" expiration time for different keys, - # and interspersed with "DEBUG SLEEP", to verify that time is frozen in script. - # The commands involved are [P]TTL / SET EX[PX] / [P]EXPIRE / GETEX / [P]SETEX / [P]EXPIRETIME - set res [run_script { - redis.call("SET", "key1{t}", "value", "EX", 1) - redis.call("DEBUG", "SLEEP", 0.01) - - redis.call("SET", "key2{t}", "value", "PX", 1000) - redis.call("DEBUG", "SLEEP", 0.01) - - redis.call("SET", "key3{t}", "value") - redis.call("EXPIRE", "key3{t}", 1) - redis.call("DEBUG", "SLEEP", 0.01) - - redis.call("SET", "key4{t}", "value") - redis.call("PEXPIRE", "key4{t}", 1000) - redis.call("DEBUG", "SLEEP", 0.01) - - redis.call("SETEX", "key5{t}", 1, "value") - redis.call("DEBUG", "SLEEP", 0.01) - - redis.call("PSETEX", "key6{t}", 1000, "value") - redis.call("DEBUG", "SLEEP", 0.01) - - redis.call("SET", "key7{t}", "value") - redis.call("GETEX", "key7{t}", "EX", 1) - redis.call("DEBUG", "SLEEP", 0.01) - - redis.call("SET", "key8{t}", "value") - redis.call("GETEX", "key8{t}", "PX", 1000) - redis.call("DEBUG", "SLEEP", 0.01) - - local ttl_results = {redis.call("TTL", "key1{t}"), - redis.call("TTL", "key2{t}"), - redis.call("TTL", "key3{t}"), - redis.call("TTL", "key4{t}"), - redis.call("TTL", "key5{t}"), - redis.call("TTL", "key6{t}"), - redis.call("TTL", "key7{t}"), - redis.call("TTL", "key8{t}")} - - local pttl_results = {redis.call("PTTL", "key1{t}"), - redis.call("PTTL", "key2{t}"), - redis.call("PTTL", "key3{t}"), - redis.call("PTTL", "key4{t}"), - redis.call("PTTL", "key5{t}"), - redis.call("PTTL", "key6{t}"), - redis.call("PTTL", "key7{t}"), - redis.call("PTTL", "key8{t}")} - - local expiretime_results = {redis.call("EXPIRETIME", "key1{t}"), - redis.call("EXPIRETIME", "key2{t}"), - redis.call("EXPIRETIME", "key3{t}"), - redis.call("EXPIRETIME", "key4{t}"), - redis.call("EXPIRETIME", "key5{t}"), - redis.call("EXPIRETIME", "key6{t}"), - redis.call("EXPIRETIME", "key7{t}"), - redis.call("EXPIRETIME", "key8{t}")} - - local pexpiretime_results = {redis.call("PEXPIRETIME", "key1{t}"), - redis.call("PEXPIRETIME", "key2{t}"), - redis.call("PEXPIRETIME", "key3{t}"), - redis.call("PEXPIRETIME", "key4{t}"), - redis.call("PEXPIRETIME", "key5{t}"), - redis.call("PEXPIRETIME", "key6{t}"), - redis.call("PEXPIRETIME", "key7{t}"), - redis.call("PEXPIRETIME", "key8{t}")} - - return {ttl_results, pttl_results, expiretime_results, pexpiretime_results} - } 8 key1{t} key2{t} key3{t} key4{t} key5{t} key6{t} key7{t} key8{t}] - - # The elements in each list are equal. - assert_equal 1 [llength [lsort -unique [lindex $res 0]]] - assert_equal 1 [llength [lsort -unique [lindex $res 1]]] - assert_equal 1 [llength [lsort -unique [lindex $res 2]]] - assert_equal 1 [llength [lsort -unique [lindex $res 3]]] - - # Then we check that the expiration time is set successfully. - assert_morethan [lindex $res 0] 0 - assert_morethan [lindex $res 1] 0 - assert_morethan [lindex $res 2] 0 - assert_morethan [lindex $res 3] 0 - } - - test "RESTORE expired keys with expiration time" { - set res [run_script { - redis.call("SET", "key1{t}", "value") - local encoded = redis.call("DUMP", "key1{t}") - - redis.call("RESTORE", "key2{t}", 1, encoded, "REPLACE") - redis.call("DEBUG", "SLEEP", 0.01) - redis.call("RESTORE", "key3{t}", 1, encoded, "REPLACE") - - return {redis.call("PEXPIRETIME", "key2{t}"), redis.call("PEXPIRETIME", "key3{t}")} - } 3 key1{t} key2{t} key3{t}] - - # Can get the expiration time and they are all equal. - assert_morethan [lindex $res 0] 0 - assert_equal [lindex $res 0] [lindex $res 1] - } - - r debug set-disable-deny-scripts 0 -} - -start_server {tags {"scripting"}} { - test "Test script flush will not leak memory - script:$is_eval" { - r flushall - r script flush - r function flush - - # This is a best-effort test to check we don't leak some resources on - # script flush and function flush commands. For lua vm, we create a - # jemalloc thread cache. On each script flush command, thread cache is - # destroyed and we create a new one. In this test, running script flush - # many times to verify there is no increase in the memory usage while - # re-creating some of the resources for lua vm. - set used_memory [s used_memory] - set allocator_allocated [s allocator_allocated] - - r multi - for {set j 1} {$j <= 500} {incr j} { - if {$is_eval} { - r SCRIPT FLUSH - } else { - r FUNCTION FLUSH - } - } - r exec - - # Verify used memory is not (much) higher. - assert_lessthan [s used_memory] [expr $used_memory*1.5] - assert_lessthan [s allocator_allocated] [expr $allocator_allocated*1.5] - } - - test "Verify Lua performs GC correctly after script loading" { - set dummy_script "--[string repeat x 10]\nreturn " - set n 50000 - for {set i 0} {$i < $n} {incr i} { - set script "$dummy_script[format "%06d" $i]" - if {$is_eval} { - r script load $script - } else { - r function load "#!lua name=test$i\nredis.register_function('test$i', function(KEYS, ARGV)\n $script \nend)" - } - } - - if {$is_eval} { - assert_lessthan [s used_memory_lua] 17500000 - } else { - assert_lessthan [s used_memory_vm_functions] 14500000 - } - } {} {debug_defrag:skip} -} -} ;# foreach is_eval - - -# Scripting "shebang" notation tests -start_server {tags {"scripting"}} { - test "Shebang support for lua engine" { - catch { - r eval {#!not-lua - return 1 - } 0 - } e - assert_match {*Unexpected engine in script shebang*} $e - - assert_equal [r eval {#!lua - return 1 - } 0] 1 - } - - test "Unknown shebang option" { - catch { - r eval {#!lua badger=data - return 1 - } 0 - } e - assert_match {*Unknown lua shebang option*} $e - } - - test "Unknown shebang flag" { - catch { - r eval {#!lua flags=allow-oom,what? - return 1 - } 0 - } e - assert_match {*Unexpected flag in script shebang*} $e - } - - test "allow-oom shebang flag" { - r set x 123 - - r config set maxmemory 1 - - # Fail to execute deny-oom command in OOM condition (backwards compatibility mode without flags) - assert_error {OOM command not allowed when used memory > 'maxmemory'*} { - r eval { - redis.call('set','x',1) - return 1 - } 1 x - } - # Can execute non deny-oom commands in OOM condition (backwards compatibility mode without flags) - assert_equal [ - r eval { - return redis.call('get','x') - } 1 x - ] {123} - - # Fail to execute regardless of script content when we use default flags in OOM condition - assert_error {OOM *} { - r eval {#!lua flags= - return 1 - } 0 - } - - # Script with allow-oom can write despite being in OOM state - assert_equal [ - r eval {#!lua flags=allow-oom - redis.call('set','x',1) - return 1 - } 1 x - ] 1 - - # read-only scripts implies allow-oom - assert_equal [ - r eval {#!lua flags=no-writes - redis.call('get','x') - return 1 - } 0 - ] 1 - assert_equal [ - r eval_ro {#!lua flags=no-writes - redis.call('get','x') - return 1 - } 1 x - ] 1 - - # Script with no shebang can read in OOM state - assert_equal [ - r eval { - redis.call('get','x') - return 1 - } 1 x - ] 1 - - # Script with no shebang can read in OOM state (eval_ro variant) - assert_equal [ - r eval_ro { - redis.call('get','x') - return 1 - } 1 x - ] 1 - - r config set maxmemory 0 - } {OK} {needs:config-maxmemory} - - test "no-writes shebang flag" { - assert_error {ERR Write commands are not allowed from read-only scripts*} { - r eval {#!lua flags=no-writes - redis.call('set','x',1) - return 1 - } 1 x - } - } - - start_server {tags {"external:skip"}} { - r -1 set x "some value" - test "no-writes shebang flag on replica" { - r replicaof [srv -1 host] [srv -1 port] - wait_for_condition 50 100 { - [s role] eq {slave} && - [string match {*master_link_status:up*} [r info replication]] - } else { - fail "Can't turn the instance into a replica" - } - - assert_equal [ - r eval {#!lua flags=no-writes - return redis.call('get','x') - } 1 x - ] "some value" - - assert_error {READONLY You can't write against a read only replica.} { - r eval {#!lua - return redis.call('get','x') - } 1 x - } - - # test no-write inside multi-exec - r multi - r eval {#!lua flags=no-writes - redis.call('get','x') - return 1 - } 1 x - assert_equal [r exec] 1 - - # test no shebang without write inside multi-exec - r multi - r eval { - redis.call('get','x') - return 1 - } 1 x - assert_equal [r exec] 1 - - # temporarily set the server to master, so it doesn't block the queuing - # and we can test the evaluation of the flags on exec - r replicaof no one - set rr [redis_client] - set rr2 [redis_client] - $rr multi - $rr2 multi - - # test write inside multi-exec - # we don't need to do any actual write - $rr eval {#!lua - return 1 - } 0 - - # test no shebang with write inside multi-exec - $rr2 eval { - redis.call('set','x',1) - return 1 - } 1 x - - r replicaof [srv -1 host] [srv -1 port] - - # To avoid -LOADING reply, wait until replica syncs with master. - wait_for_condition 50 100 { - [s master_link_status] eq {up} - } else { - fail "Replica did not sync in time." - } - - assert_error {EXECABORT Transaction discarded because of: READONLY *} {$rr exec} - assert_error {READONLY You can't write against a read only replica. script: *} {$rr2 exec} - $rr close - $rr2 close - } - } - - test "not enough good replicas" { - r set x "some value" - r config set min-replicas-to-write 1 - - assert_equal [ - r eval {#!lua flags=no-writes - return redis.call('get','x') - } 1 x - ] "some value" - - assert_equal [ - r eval { - return redis.call('get','x') - } 1 x - ] "some value" - - assert_error {NOREPLICAS *} { - r eval {#!lua - return redis.call('get','x') - } 1 x - } - - assert_error {NOREPLICAS *} { - r eval { - return redis.call('set','x', 1) - } 1 x - } - - r config set min-replicas-to-write 0 - } - - test "not enough good replicas state change during long script" { - r set x "pre-script value" - r config set min-replicas-to-write 1 - r config set lua-time-limit 10 - start_server {tags {"external:skip"}} { - # add a replica and wait for the master to recognize it's online - r slaveof [srv -1 host] [srv -1 port] - wait_replica_online [srv -1 client] - - # run a slow script that does one write, then waits for INFO to indicate - # that the replica dropped, and then runs another write - set rd [redis_deferring_client -1] - $rd eval { - redis.call('set','x',"script value") - while true do - local info = redis.call('info','replication') - if (string.match(info, "connected_slaves:0")) then - redis.call('set','x',info) - break - end - end - return 1 - } 1 x - - # wait for the script to time out and yield - wait_for_condition 100 100 { - [catch {r -1 ping} e] == 1 - } else { - fail "Can't wait for script to start running" - } - catch {r -1 ping} e - assert_match {BUSY*} $e - - # cause the replica to disconnect (triggering the busy script to exit) - r slaveof no one - - # make sure the script was able to write after the replica dropped - assert_equal [$rd read] 1 - assert_match {*connected_slaves:0*} [r -1 get x] - - $rd close - } - r config set min-replicas-to-write 0 - r config set lua-time-limit 5000 - } {OK} {external:skip needs:repl} - - test "allow-stale shebang flag" { - r config set replica-serve-stale-data no - r replicaof 127.0.0.1 1 - - assert_error {MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.} { - r eval { - return redis.call('get','x') - } 1 x - } - - assert_error {MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.} { - r eval {#!lua flags=no-writes - return 1 - } 0 - } - - assert_equal [ - r eval {#!lua flags=allow-stale,no-writes - return 1 - } 0 - ] 1 - - - assert_error {*Can not execute the command on a stale replica*} { - r eval {#!lua flags=allow-stale,no-writes - return redis.call('get','x') - } 1 x - } - - assert_match {foobar} [ - r eval {#!lua flags=allow-stale,no-writes - return redis.call('echo','foobar') - } 0 - ] - - # Test again with EVALSHA - set sha [ - r script load {#!lua flags=allow-stale,no-writes - return redis.call('echo','foobar') - } - ] - assert_match {foobar} [r evalsha $sha 0] - - r replicaof no one - r config set replica-serve-stale-data yes - set _ {} - } {} {external:skip} - - test "reject script do not cause a Lua stack leak" { - r config set maxmemory 1 - for {set i 0} {$i < 50} {incr i} { - assert_error {OOM *} {r eval {#!lua - return 1 - } 0} - } - r config set maxmemory 0 - assert_equal [r eval {#!lua - return 1 - } 0] 1 - } -} - -# Additional eval only tests -start_server {tags {"scripting"}} { - test "Consistent eval error reporting" { - r config resetstat - r config set maxmemory 1 - # Script aborted due to Redis state (OOM) should report script execution error with detailed internal error - assert_error {OOM command not allowed when used memory > 'maxmemory'*} { - r eval {return redis.call('set','x','y')} 1 x - } - assert_equal [errorrstat OOM r] {count=1} - assert_equal [s total_error_replies] {1} - assert_match {calls=0*rejected_calls=1,failed_calls=0*} [cmdrstat set r] - assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat eval r] - - # redis.pcall() failure due to Redis state (OOM) returns lua error table with Redis error message without '-' prefix - r config resetstat - assert_equal [ - r eval { - local t = redis.pcall('set','x','y') - if t['err'] == "OOM command not allowed when used memory > 'maxmemory'." then - return 1 - else - return 0 - end - } 1 x - ] 1 - # error stats were not incremented - assert_equal [errorrstat ERR r] {} - assert_equal [errorrstat OOM r] {count=1} - assert_equal [s total_error_replies] {1} - assert_match {calls=0*rejected_calls=1,failed_calls=0*} [cmdrstat set r] - assert_match {calls=1*rejected_calls=0,failed_calls=0*} [cmdrstat eval r] - - # Returning an error object from lua is handled as a valid RESP error result. - r config resetstat - assert_error {OOM command not allowed when used memory > 'maxmemory'.} { - r eval { return redis.pcall('set','x','y') } 1 x - } - assert_equal [errorrstat ERR r] {} - assert_equal [errorrstat OOM r] {count=1} - assert_equal [s total_error_replies] {1} - assert_match {calls=0*rejected_calls=1,failed_calls=0*} [cmdrstat set r] - assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat eval r] - - r config set maxmemory 0 - r config resetstat - # Script aborted due to error result of Redis command - assert_error {ERR DB index is out of range*} { - r eval {return redis.call('select',99)} 0 - } - assert_equal [errorrstat ERR r] {count=1} - assert_equal [s total_error_replies] {1} - assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat select r] - assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat eval r] - - # redis.pcall() failure due to error in Redis command returns lua error table with redis error message without '-' prefix - r config resetstat - assert_equal [ - r eval { - local t = redis.pcall('select',99) - if t['err'] == "ERR DB index is out of range" then - return 1 - else - return 0 - end - } 0 - ] 1 - assert_equal [errorrstat ERR r] {count=1} ; - assert_equal [s total_error_replies] {1} - assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat select r] - assert_match {calls=1*rejected_calls=0,failed_calls=0*} [cmdrstat eval r] - - # Script aborted due to scripting specific error state (write cmd with eval_ro) should report script execution error with detailed internal error - r config resetstat - assert_error {ERR Write commands are not allowed from read-only scripts*} { - r eval_ro {return redis.call('set','x','y')} 1 x - } - assert_equal [errorrstat ERR r] {count=1} - assert_equal [s total_error_replies] {1} - assert_match {calls=0*rejected_calls=1,failed_calls=0*} [cmdrstat set r] - assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat eval_ro r] - - # redis.pcall() failure due to scripting specific error state (write cmd with eval_ro) returns lua error table with Redis error message without '-' prefix - r config resetstat - assert_equal [ - r eval_ro { - local t = redis.pcall('set','x','y') - if t['err'] == "ERR Write commands are not allowed from read-only scripts." then - return 1 - else - return 0 - end - } 1 x - ] 1 - assert_equal [errorrstat ERR r] {count=1} - assert_equal [s total_error_replies] {1} - assert_match {calls=0*rejected_calls=1,failed_calls=0*} [cmdrstat set r] - assert_match {calls=1*rejected_calls=0,failed_calls=0*} [cmdrstat eval_ro r] - - r config resetstat - # make sure geoadd will failed - r set Sicily 1 - assert_error {WRONGTYPE Operation against a key holding the wrong kind of value*} { - r eval {return redis.call('GEOADD', 'Sicily', '13.361389', '38.115556', 'Palermo', '15.087269', '37.502669', 'Catania')} 1 x - } - assert_equal [errorrstat WRONGTYPE r] {count=1} - assert_equal [s total_error_replies] {1} - assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat geoadd r] - assert_match {calls=1*rejected_calls=0,failed_calls=1*} [cmdrstat eval r] - } {} {cluster:skip} - - test "LUA redis.error_reply API" { - r config resetstat - assert_error {MY_ERR_CODE custom msg} { - r eval {return redis.error_reply("MY_ERR_CODE custom msg")} 0 - } - assert_equal [errorrstat MY_ERR_CODE r] {count=1} - } - - test "LUA redis.error_reply API with empty string" { - r config resetstat - assert_error {ERR} { - r eval {return redis.error_reply("")} 0 - } - assert_equal [errorrstat ERR r] {count=1} - } - - test "LUA redis.status_reply API" { - r config resetstat - r readraw 1 - assert_equal [ - r eval {return redis.status_reply("MY_OK_CODE custom msg")} 0 - ] {+MY_OK_CODE custom msg} - r readraw 0 - assert_equal [errorrstat MY_ERR_CODE r] {} ;# error stats were not incremented - } - - test "LUA test pcall" { - assert_equal [ - r eval {local status, res = pcall(function() return 1 end); return 'status: ' .. tostring(status) .. ' result: ' .. res} 0 - ] {status: true result: 1} - } - - test "LUA test pcall with error" { - assert_match {status: false result:*Script attempted to access nonexistent global variable 'foo'} [ - r eval {local status, res = pcall(function() return foo end); return 'status: ' .. tostring(status) .. ' result: ' .. res} 0 - ] - } - - test "LUA test pcall with non string/integer arg" { - assert_error "ERR Lua redis lib command arguments must be strings or integers*" { - r eval { - local x={} - return redis.call("ping", x) - } 0 - } - # run another command, to make sure the cached argv array survived - assert_equal [ - r eval { - return redis.call("ping", "asdf") - } 0 - ] {asdf} - } - - test "LUA test trim string as expected" { - # this test may fail if we use different memory allocator than jemalloc, as libc for example may keep the old size on realloc. - if {[string match {*jemalloc*} [s mem_allocator]]} { - # test that when using LUA cache mechanism, if there is free space in the argv array, the string is trimmed. - r set foo [string repeat "a" 45] - set expected_memory [r memory usage foo] - - # Jemalloc will allocate for the requested 63 bytes, 80 bytes. - # We can't test for larger sizes because LUA_CMD_OBJCACHE_MAX_LEN is 64. - # This value will be recycled to be used in the next argument. - # We use SETNX to avoid saving the string which will prevent us to reuse it in the next command. - r eval { - return redis.call("SETNX", "foo", string.rep("a", 63)) - } 0 - - # Jemalloc will allocate for the request 45 bytes, 56 bytes. - # we can't test for smaller sizes because OBJ_ENCODING_EMBSTR_SIZE_LIMIT is 44 where no trim is done. - r eval { - return redis.call("SET", "foo", string.rep("a", 45)) - } 0 - - # Assert the string has been trimmed and the 80 bytes from the previous alloc were not kept. - assert { [r memory usage foo] <= $expected_memory}; - } - } - - test {EVAL - explicit error() call handling} { - # error("simple string error") - assert_error {ERR user_script:1: simple string error script: *} { - r eval "error('simple string error')" 0 - } - - # error({"err": "ERR table error"}) - assert_error {ERR table error script: *} { - r eval "error({err='ERR table error'})" 0 - } - - # error({}) - assert_error {ERR unknown error script: *} { - r eval "error({})" 0 - } - } -} |
