mirror of
https://github.com/valkey-io/valkey.git
synced 2026-05-07 22:09:30 -04:00
29bf862bcc
This PR restructures the Lua scripting functionality by extracting it from the core Valkey server into a separate Valkey module. This change enables the possibility of a backwards compatible Lua engine upgrade, as well as, the flexibility in building Valkey without the Lua engine. **Important**: from a user's point of view, there's no difference in using the `EVAL` of `FUNCTION/FCALL` scripts. This PR is fully backward compatible with respect to the public API. The main code change is the move and adaptation of the Lua engine source files from `src/lua` to `src/modules/lua`. The original Lua engine code is adapted to use the module API to compile and execute scripts. The main difference between the original code and the new, is the serialization and deserialization of Valkey RESP values into, and from, Lua values. While in the original implementation the parsing of RESP values was done directly from the client buffer, in the new implementation the parsing is done from the `ValkeyModuleCallReply` object and respective API. The Makefile and CMake build systems were also updated to build and integrate the new Lua engine module, within the Valkey server build workflow. When the Valkey server is built, the Lua engine module is also built, and, the Lua module is loaded automatically by the server upon startup. When running `make install` the Lua engine module is installed in the default system library directory. There's a new build option, called `BUILD_LUA`, that if set to `no` allows to build Valkey server without building the Lua engine. This modular architecture enables future development of additional Lua engine modules with newer Lua versions that can be loaded alongside the current engine, facilitating gradual migration paths for users. Additional change: Unload all modules on shutdown (ignoring modules that can't be unloaded). This is to avoid address sanitizer warnings about leaked allocations. Fixes: #1627 --------- Signed-off-by: Ricardo Dias <ricardo.dias@percona.com>
503 lines
16 KiB
Tcl
503 lines
16 KiB
Tcl
set testmodule [file normalize tests/modules/helloscripting.so]
|
|
|
|
set HELLO_PROGRAM "#!hello name=mylib\nRFUNCTION foo\nARGS 0\nRETURN\nFUNCTION bar\nCONSTI 432\nRETURN"
|
|
|
|
start_server {tags {"modules"}} {
|
|
r module load $testmodule
|
|
|
|
r function load $HELLO_PROGRAM
|
|
|
|
test {Load script with invalid library name} {
|
|
assert_error {ERR Library names can only contain letters, numbers, or underscores(_) and must be at least one character long} {r function load "#!hello name=my-lib\nFUNCTION foo\nARGS 0\nRETURN"}
|
|
}
|
|
|
|
test {Load script with existing library} {
|
|
assert_error {ERR Library 'mylib' already exists} {r function load $HELLO_PROGRAM}
|
|
}
|
|
|
|
test {Load script with invalid engine} {
|
|
assert_error {ERR Engine 'wasm' not found} {r function load "#!wasm name=mylib2\nFUNCTION foo\nARGS 0\nRETURN"}
|
|
}
|
|
|
|
test {Load script with no functions} {
|
|
assert_error {ERR No functions registered} {r function load "#!hello name=mylib2\n"}
|
|
}
|
|
|
|
test {Load script with duplicate function} {
|
|
assert_error {ERR Function foo already exists} {r function load "#!hello name=mylib2\nFUNCTION foo\nARGS 0\nRETURN"}
|
|
}
|
|
|
|
test {Load script with no metadata header} {
|
|
assert_error {ERR Missing library metadata} {r function load "FUNCTION foo\nARGS 0\nRETURN"}
|
|
}
|
|
|
|
test {Load script with header without lib name} {
|
|
assert_error {ERR Library name was not given} {r function load "#!hello \n"}
|
|
}
|
|
|
|
test {Load script with header with unknown param} {
|
|
assert_error {ERR Invalid metadata value given: nme=mylib} {r function load "#!hello nme=mylib\n"}
|
|
}
|
|
|
|
test {Load script with header with lib name passed twice} {
|
|
assert_error {ERR Invalid metadata value, name argument was given multiple times} {r function load "#!hello name=mylib2 name=mylib3\n"}
|
|
}
|
|
|
|
test {Load script with invalid function name} {
|
|
assert_error {ERR Function names can only contain letters, numbers, or underscores(_) and must be at least one character long} {r function load "#!hello name=mylib2\nFUNCTION foo-bar\nARGS 0\nRETURN"}
|
|
}
|
|
|
|
test {Load script with duplicate function} {
|
|
assert_error {ERR Function already exists in the library} {r function load "#!hello name=mylib2\nFUNCTION foo\nARGS 0\nRETURN\nFUNCTION foo\nARGS 0\nRETURN"}
|
|
}
|
|
|
|
test {Load script with syntax error} {
|
|
assert_error {ERR Failed to parse instruction: 'SEND'} {r function load replace "#!hello name=mylib3\nFUNCTION foo\nARGS 0\nSEND"}
|
|
}
|
|
|
|
test {Call scripting engine function: calling foo works} {
|
|
r fcall foo 0 134
|
|
} {134}
|
|
|
|
test {Call scripting engine function: calling bar works} {
|
|
r fcall bar 0
|
|
} {432}
|
|
|
|
test {Call server command from script} {
|
|
set result [r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTS x
|
|
ARGS 0
|
|
CONSTI 2
|
|
CALL SET
|
|
RETURN
|
|
} 0 43]
|
|
assert_equal $result "OK"
|
|
assert_equal [r GET x] 43
|
|
|
|
set result [r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTS hello
|
|
CONSTI 1
|
|
CALL PING
|
|
RETURN
|
|
} 0]
|
|
assert_equal $result "hello"
|
|
|
|
set result [r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTI 0
|
|
CALL PING
|
|
RETURN
|
|
} 0]
|
|
assert_equal $result "PONG"
|
|
}
|
|
|
|
test {Call server NOSCRIPT command} {
|
|
assert_error {ERR command 'acl|cat' is not allowed on script mode} {
|
|
r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTS CAT
|
|
CONSTI 1
|
|
CALL ACL
|
|
RETURN
|
|
} 0
|
|
}
|
|
|
|
assert_error {ERR This Valkey command is not allowed from script*} {
|
|
r eval {#!lua
|
|
return server.call('ACL', 'CAT')
|
|
} 0
|
|
}
|
|
|
|
r debug set-disable-deny-scripts 1
|
|
|
|
set result [r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTS CAT
|
|
CONSTI 1
|
|
CALL ACL
|
|
RETURN
|
|
} 0]
|
|
assert_equal $result "OK"
|
|
|
|
r eval {#!lua
|
|
return server.call('ACL', 'CAT')
|
|
} 0
|
|
|
|
r debug set-disable-deny-scripts 0
|
|
}
|
|
|
|
test {Call server command without permission} {
|
|
r acl setuser default -set
|
|
|
|
r ACL LOG RESET
|
|
|
|
assert_error {NOPERM User default has no permissions *} {r set x 5}
|
|
|
|
assert_error {NOPERM User default has no permissions *} {
|
|
r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTS x
|
|
ARGS 0
|
|
CONSTI 2
|
|
CALL SET
|
|
RETURN
|
|
} 0 43
|
|
}
|
|
|
|
assert_error {ERR ACL failure in script*} {
|
|
r eval {#!lua
|
|
return server.call('SET', 'x', 5)
|
|
} 0
|
|
}
|
|
|
|
# verify ACL LOG entries
|
|
set entries [r ACL LOG]
|
|
assert_equal [llength $entries] 3
|
|
|
|
set entry [lindex $entries 0]
|
|
assert_equal [dict get $entry username] {default}
|
|
assert_equal [dict get $entry context] {lua}
|
|
assert_equal [dict get $entry object] {set}
|
|
assert_equal [dict get $entry reason] {command}
|
|
assert_match {*cmd=eval*} [dict get $entry client-info]
|
|
|
|
set entry [lindex $entries 1]
|
|
assert_equal [dict get $entry username] {default}
|
|
assert_equal [dict get $entry context] {script}
|
|
assert_equal [dict get $entry object] {set}
|
|
assert_equal [dict get $entry reason] {command}
|
|
assert_match {*cmd=eval*} [dict get $entry client-info]
|
|
|
|
set entry [lindex $entries 2]
|
|
assert_equal [dict get $entry username] {default}
|
|
assert_equal [dict get $entry context] {toplevel}
|
|
assert_equal [dict get $entry object] {set}
|
|
assert_equal [dict get $entry reason] {command}
|
|
assert_match {*cmd=set*} [dict get $entry client-info]
|
|
|
|
r acl setuser default +set
|
|
}
|
|
|
|
test {Call server write command in RO script} {
|
|
assert_error {ERR Write commands are not allowed*} {
|
|
r eval {#!lua flags=no-writes
|
|
return server.call('SET', 'x', 5)
|
|
} 0
|
|
}
|
|
|
|
assert_error {ERR Write commands are not allowed*} {
|
|
r eval {#!hello flags=no-writes
|
|
FUNCTION callcmd
|
|
CONSTS x
|
|
CONSTI 43
|
|
CONSTI 2
|
|
CALL SET
|
|
RETURN
|
|
} 0
|
|
}
|
|
|
|
r function load {#!hello name=errlib
|
|
RFUNCTION callcmd
|
|
CONSTS x
|
|
CONSTI 43
|
|
CONSTI 2
|
|
CALL SET
|
|
RETURN
|
|
}
|
|
assert_error {ERR Write commands are not allowed*} {r fcall callcmd 0}
|
|
}
|
|
|
|
test {Call server command when OOM} {
|
|
r config set maxmemory 1
|
|
|
|
assert_error {*command not allowed when used memory*} {
|
|
r eval {#!lua
|
|
return server.call('set', 'x', 1)
|
|
} 0
|
|
}
|
|
|
|
set res [r eval {#!lua flags=allow-oom
|
|
return server.call('set', 'x', 1)
|
|
} 0]
|
|
assert_equal $res "OK"
|
|
|
|
assert_error {*command not allowed when used memory*} {
|
|
r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTS x
|
|
CONSTI 43
|
|
CONSTI 2
|
|
CALL SET
|
|
RETURN
|
|
} 0
|
|
}
|
|
|
|
set res [r eval {#!hello flags=allow-oom
|
|
FUNCTION callcmd
|
|
CONSTS x
|
|
CONSTI 43
|
|
CONSTI 2
|
|
CALL SET
|
|
RETURN
|
|
} 0]
|
|
assert_equal $res "OK"
|
|
|
|
r config set maxmemory 0
|
|
}
|
|
|
|
test {Ensure errors from commands called from script is counted only once} {
|
|
r lpush l 1
|
|
assert_error {WRONGTYPE Operation against a key holding the wrong kind of value*} {
|
|
r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTS l
|
|
CONSTI 1
|
|
CALL GET
|
|
RETURN
|
|
} 0
|
|
}
|
|
|
|
set errorstats [r info Errorstats]
|
|
regexp {errorstat_WRONGTYPE:count=([0-9]+)} $errorstats -> wrongtype_errors
|
|
assert_equal $wrongtype_errors 1
|
|
|
|
}
|
|
|
|
test {Call server command that returns NULL values} {
|
|
r lpush s a
|
|
|
|
set result [r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTS s
|
|
CONSTI 1
|
|
CONSTI 2
|
|
CALL BLPOP
|
|
RETURN
|
|
} 0]
|
|
assert_equal $result "OK"
|
|
|
|
set result [r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTS s
|
|
CONSTI 2
|
|
CONSTI 2
|
|
CALL BLPOP
|
|
RETURN
|
|
} 0]
|
|
assert_equal $result "(null array)"
|
|
|
|
set result [r eval {#!hello
|
|
FUNCTION callcmd
|
|
CONSTS f
|
|
CONSTI 1
|
|
CALL GET
|
|
RETURN
|
|
} 0]
|
|
assert_equal $result "(null string)"
|
|
}
|
|
|
|
|
|
test {Replace function library and call functions} {
|
|
set result [r function load replace "#!hello name=mylib\nFUNCTION foo\nARGS 0\nRETURN\nFUNCTION bar\nCONSTI 500\nRETURN"]
|
|
assert_equal $result "mylib"
|
|
|
|
set result [r fcall foo 0 132]
|
|
assert_equal $result 132
|
|
|
|
set result [r fcall bar 0]
|
|
assert_equal $result 500
|
|
}
|
|
|
|
test {List scripting engine functions} {
|
|
r function flush sync
|
|
r function load replace "#!hello name=mylib\nFUNCTION foobar\nARGS 0\nRETURN"
|
|
r function list
|
|
} {{library_name mylib engine HELLO functions {{name foobar description {} flags {}}}}}
|
|
|
|
test {Load a second library and call a function} {
|
|
r function load "#!hello name=mylib2\nFUNCTION getarg\nARGS 0\nRETURN"
|
|
set result [r fcall getarg 0 456]
|
|
assert_equal $result 456
|
|
}
|
|
|
|
test {Delete all libraries and functions} {
|
|
set result [r function flush]
|
|
assert_equal $result {OK}
|
|
r function list
|
|
} {}
|
|
|
|
test {Test the deletion of a single library} {
|
|
r function load $HELLO_PROGRAM
|
|
r function load "#!hello name=mylib2\nFUNCTION getarg\nARGS 0\nRETURN"
|
|
|
|
set result [r function delete mylib]
|
|
assert_equal $result {OK}
|
|
|
|
set result [r fcall getarg 0 446]
|
|
assert_equal $result 446
|
|
}
|
|
|
|
test {Test dump and restore function library} {
|
|
r function load $HELLO_PROGRAM
|
|
|
|
set result [r fcall bar 0]
|
|
assert_equal $result 432
|
|
|
|
set dump [r function dump]
|
|
|
|
set result [r function flush]
|
|
assert_equal $result {OK}
|
|
|
|
set result [r function restore $dump]
|
|
assert_equal $result {OK}
|
|
|
|
set result [r fcall getarg 0 436]
|
|
assert_equal $result 436
|
|
|
|
set result [r fcall bar 0]
|
|
assert_equal $result 432
|
|
}
|
|
|
|
test {Test function kill} {
|
|
set rd [valkey_deferring_client]
|
|
r config set busy-reply-threshold 10
|
|
r function load REPLACE "#!hello name=mylib\nFUNCTION wait\nARGS 0\nSLEEP\nARGS 0\nRETURN"
|
|
$rd fcall wait 0 100
|
|
after 1000
|
|
catch {r ping} e
|
|
assert_match {BUSY*} $e
|
|
assert_match {running_script {name wait command {fcall wait 0 100} duration_ms *} engines {*}} [r FUNCTION STATS]
|
|
r function kill
|
|
after 1000 ;
|
|
assert_equal [r ping] "PONG"
|
|
assert_error {ERR Script killed by user with FUNCTION KILL*} {$rd read}
|
|
$rd ping
|
|
assert_equal [$rd read] "PONG"
|
|
$rd close
|
|
}
|
|
|
|
test {Test eval execution} {
|
|
set result [r eval "#!hello\nFUNCTION foo\nARGS 0\nRETURN" 0 145]
|
|
assert_equal $result 145
|
|
}
|
|
|
|
test {Test evalsha execution} {
|
|
set sha [r script load "#!hello\nFUNCTION foo\nARGS 0\nRETURN"]
|
|
set result [r evalsha $sha 0 167]
|
|
assert_equal $result 167
|
|
}
|
|
|
|
test {Test script exists} {
|
|
set sha [r script load "#!hello\nFUNCTION foo\nARGS 0\nRETURN"]
|
|
set result [r script exists $sha]
|
|
assert_equal $result 1
|
|
}
|
|
|
|
test {Test script flush sync} {
|
|
set sha [r script load "#!hello\nFUNCTION foo\nARGS 0\nRETURN"]
|
|
set result [r script exists $sha]
|
|
assert_equal $result 1
|
|
r script flush SYNC
|
|
set result [r script exists $sha]
|
|
assert_equal $result 0
|
|
}
|
|
|
|
test {Test script flush async} {
|
|
set sha [r script load "#!hello\nFUNCTION foo\nARGS 0\nRETURN"]
|
|
set result [r script exists $sha]
|
|
assert_equal $result 1
|
|
r script flush ASYNC
|
|
set result [r script exists $sha]
|
|
assert_equal $result 0
|
|
}
|
|
|
|
test {Test HELLO debugger} {
|
|
r script debug sync hello
|
|
set ret [r eval "#!hello\nFUNCTION foo\nARGS 0\nRETURN" 0 167]
|
|
assert_equal {{>>> 0: ARGS 0}} $ret
|
|
set cmd "*1\r\n\$4\r\nstep\r\n"
|
|
r write $cmd
|
|
r flush
|
|
set ret [r read]
|
|
assert_equal {{>>> 1: RETURN}} $ret
|
|
set cmd "*1\r\n\$5\r\nstack\r\n"
|
|
r write $cmd
|
|
r flush
|
|
set ret [r read]
|
|
assert_equal {{Stack contents:} {top -> [0] 167}} $ret
|
|
set cmd "*1\r\n\$1\r\nc\r\n"
|
|
r write $cmd
|
|
r flush
|
|
set ret [r read]
|
|
assert_equal {<endsession>} $ret
|
|
r script debug off
|
|
reconnect
|
|
assert_equal [r ping] {PONG}
|
|
}
|
|
|
|
test {Test INFO scriptingengines section} {
|
|
# Get the scripting engines info section
|
|
set info [r info scriptingengines]
|
|
|
|
# Verify the section header exists
|
|
assert_match "*# Scripting Engines*" $info
|
|
|
|
# Verify we have exactly 2 engines (LUA + HELLO)
|
|
assert_match "*engines_count:*" $info
|
|
regexp {engines_count:([0-9]+)} $info -> engines_count
|
|
assert_equal $engines_count 2
|
|
|
|
# Verify memory fields exist and are non-negative numbers
|
|
assert_match "*engines_total_used_memory:*" $info
|
|
assert_match "*engines_total_memory_overhead:*" $info
|
|
regexp {engines_total_used_memory:([0-9]+)} $info -> total_memory
|
|
regexp {engines_total_memory_overhead:([0-9]+)} $info -> total_overhead
|
|
assert {$total_memory >= 0}
|
|
assert {$total_overhead >= 0}
|
|
|
|
# Verify individual engine information exists
|
|
assert_match "*engine_0:*" $info
|
|
assert_match "*engine_1:*" $info
|
|
|
|
# Check that engines have proper format including abi_version
|
|
assert_match "*engine_*:name=*,module=*,abi_version=*,used_memory=*,memory_overhead=*" $info
|
|
|
|
# Verify both LUA and HELLO engines are present
|
|
assert_match "*name=LUA*" $info
|
|
assert_match "*name=HELLO*" $info
|
|
|
|
# Verify LUA is built-in and HELLO is from module
|
|
assert_match "*name=LUA,module=lua*" $info
|
|
assert_match "*name=HELLO,module=helloengine*" $info
|
|
}
|
|
|
|
test {Unload scripting engine module} {
|
|
set result [r module unload helloengine]
|
|
assert_equal $result "OK"
|
|
}
|
|
|
|
test {Load scripting engine in version before function env reset} {
|
|
r module load $testmodule 2
|
|
r function load $HELLO_PROGRAM
|
|
set result [r fcall foo 0 123]
|
|
assert_equal $result 123
|
|
set result [r function flush async]
|
|
assert_equal $result {OK}
|
|
assert_error {ERR Function not found} {r fcall foo 0 123}
|
|
set result [r module unload helloengine]
|
|
assert_equal $result "OK"
|
|
}
|
|
|
|
test {Load scripting engine in version before debugger support} {
|
|
r module load $testmodule 3
|
|
r function load $HELLO_PROGRAM
|
|
set result [r fcall foo 0 123]
|
|
assert_equal $result 123
|
|
assert_error {ERR The scripting engine 'HELLO' does not support interactive script debugging} {r script debug sync hello}
|
|
set result [r module unload helloengine]
|
|
assert_equal $result "OK"
|
|
}
|
|
}
|