We’ve already covered the basics of Redis Lua, and if you can write some simple Lua scripts, you’re ready to graduate from Lua.
In college courses, we mainly learn Lua script debugging and Lua execution principle in Redis.
Lua script debugging
Redis supports Lua script debugging since version 3.2. The debugger is called LDB. It has some important features:
- It uses server-client mode, so remote debugging. Redis server is the debugging server, the default client is Redis – CLI. You can also develop other clients that follow the server protocol.
- By default, each debugging session is a new session. This means that the server will not be blocked during debugging. It can still be used by other clients or open new sessions. It also means that all changes made during debugging are rolled back at the end.
- If necessary, the debugging mode can be set to synchronous so that changes to the data set can be preserved. In this mode, the server is blocked while debugging.
- Step execution is supported
- Static and dynamic breakpoints are supported
- Supports printing debug logs from scripts to the debug console
- Check Lua variables
- Track the execution of Redis commands
- Good support for printing Redis and Lua values
- Infinite loop and long execution detection, simulating breakpoints
Lua script debugging actual combat
To start debugging, write a simple Lua script script.lua:
local src = KEYS[1]
local dst = KEYS[2]
local count = tonumber(ARGV[1])
while count > 0 do
local item = redis.call('rpop',src)
if item ~= false then
redis.call('lpush',dst,item)
end
count = count - 1
end
return redis.call('llen',dst)
Copy the code
This script inserts elements from SRC into the DST header in turn.
Now that we have this script, we can start debugging.
We can run the script using the redis-cli-eval command, and to debug with the -ldb parameter, so we first execute the following command:
redis-cli --ldb --eval script.lua foo bar , 10
Copy the code
The page displays some help information and goes into debug mode
You can see the help page to tell us
- Run the quit command to exit the debugging mode
- Run the restart command to restart debugging
- To view more help information, run help
Here we run the help command to view the help information and print out a number of commands that can be executed in debug mode. The contents in parentheses “[]” are shorthand for the commands.
Common ones are:
- Step /next: Execute a line
- Continue: Execute to a breakpoint west
- List: displays the source code
- Print: Prints some values
- Break it off
You can also add dynamic breakpoints in scripts using redis.breakpoint().
Here’s a quick demonstration
Now I delete the line count = count – 1 from the code to make the program loop, and debug again
You can see that we don’t have an interruption point, but the program still stops because the execution times out, and the debugger simulates a breakpoint to stop the program. As you can see from the source, the timeout here is 5s.
/* Check if a timeout occurred. */
if (ar->event == LUA_HOOKCOUNT && ldb.step == 0 && bp == 0) {
mstime_t elapsed = mstime() - server.lua_time_start;
mstime_t timelimit = server.lua_time_limit ?
server.lua_time_limit : 5000;
if (elapsed >= timelimit) {
timeout = 1;
ldb.step = 1;
} else {
return; /* No timeout, ignore the COUNT event. */}}Copy the code
Because the default Redis debug mode is asynchronous, data in Redis is not changed after debugging.
Of course, you can choose to execute in synchronous mode by changing the ** -ldb parameter in the command to –ldb-sync-mode**.
Interpreting the EVAL command
The EVAL command has already been introduced in detail. For those who don’t know, please review the Redis Lua Script tutorial (part 1). Today we’ll explore the EVAL command in conjunction with the source code.
In the server.c file, we know that the eval command executes the evalCommand function. The implementation of this function is in scripting. C.
The function call stack is
evalCommand
(evalGenericCommandWithDebugging)
evalGenericCommand
lua_pcall / / the Lua function
Copy the code
EvalCommand function is very simple, just simple judgment is a debug mode, if it is a debug mode, call evalGenericCommandWithDebugging function, if not, call evalGenericCommand function directly.
In the evalGenericCommand function, the correct number of keys is first checked
/* Get the number of arguments that are keys */
if (getLongLongFromObjectOrReply(c,c->argv[2],&numkeys,NULL) != C_OK)
return;
if (numkeys > (c->argc - 3)) {
addReplyError(c,"Number of keys can't be greater than number of args");
return;
} else if (numkeys < 0) {
addReplyError(c,"Number of keys can't be negative");
return;
}
Copy the code
It then checks to see if the script is already in the cache, if not, evaluates the SHA1 checksum of the script, and converts the SHA1 checksum to lowercase if it already exists
/* We obtain the script SHA1, then check if this function is already * defined into the Lua state */
funcname[0] = 'f';
funcname[1] = '_';
if(! evalsha) {/* Hash the code if this is an EVAL call */
sha1hex(funcname+2,c->argv[1]->ptr,sdslen(c->argv[1]->ptr));
} else {
/* We already have the SHA if it is a EVALSHA */
int j;
char *sha = c->argv[1]->ptr;
/* Convert to lowercase. We don't use tolower since the function * managed to always show up in the profiler output consuming * a non trivial amount of time. */
for (j = 0; j < 40; j++)
funcname[j+2] = (sha[j] >= 'A' && sha[j] <= 'Z')? sha[j]+('a'-'A') : sha[j];
funcname[42] = '\ 0';
}
Copy the code
Here the funcname variable stores the f_ +SHA1 checksum, Redis will define the script as a Lua function, and funcName is the function name. The function body is the script itself.
sds luaCreateFunction(client *c, lua_State *lua, robj *body) {
char funcname[43];
dictEntry *de;
funcname[0] = 'f';
funcname[1] = '_';
sha1hex(funcname+2,body->ptr,sdslen(body->ptr));
sds sha = sdsnewlen(funcname+2.40);
if((de = dictFind(server.lua_scripts,sha)) ! =NULL) {
sdsfree(sha);
return dictGetKey(de);
}
sds funcdef = sdsempty();
funcdef = sdscat(funcdef,"function ");
funcdef = sdscatlen(funcdef,funcname,42);
funcdef = sdscatlen(funcdef,"()".3);
funcdef = sdscatlen(funcdef,body->ptr,sdslen(body->ptr));
funcdef = sdscatlen(funcdef,"\nend".4);
if (luaL_loadbuffer(lua,funcdef,sdslen(funcdef),"@user_script")) {
if(c ! =NULL) {
addReplyErrorFormat(c,
"Error compiling script (new function): %s\n",
lua_tostring(lua,- 1));
}
lua_pop(lua,1);
sdsfree(sha);
sdsfree(funcdef);
return NULL;
}
sdsfree(funcdef);
if (lua_pcall(lua,0.0.0)) {
if(c ! =NULL) {
addReplyErrorFormat(c,"Error running script (new function): %s\n",
lua_tostring(lua,- 1));
}
lua_pop(lua,1);
sdsfree(sha);
return NULL;
}
/* We also save a SHA1 -> Original script map in a dictionary * so that we can replicate / write in the AOF all the * EVALSHA commands as EVAL using the original script. */
int retval = dictAdd(server.lua_scripts,sha,body);
serverAssertWithInfo(c ? c : server.lua_client,NULL,retval == DICT_OK);
server.lua_scripts_mem += sdsZmallocSize(sha) + getStringObjectSdsUsedMemory(body);
incrRefCount(body);
return sha;
}
Copy the code
Before executing the script, also save the parameters passed in and select the correct database.
/* Populate the argv and keys table accordingly to the arguments that * EVAL received. */
luaSetGlobalArray(lua,"KEYS",c->argv+3,numkeys);
luaSetGlobalArray(lua,"ARGV",c->argv+3+numkeys,c->argc- 3-numkeys);
/* Select the right DB in the context of the Lua client */
selectDb(server.lua_client,c->db->id);
Copy the code
Then you need to set up hooks, which are used to automatically interrupt script timeouts and to stop the script with the SCRPIT KILL command and the server with the SHUTDOWN command.
/* Set a hook in order to be able to stop the script execution if it * is running for too much time. * We set the hook only if the time limit is enabled as the hook will * make the Lua script execution slower. * * If we are debugging, we set instead a "line" hook so that the * debugger is call-back at every line executed by the script. */
server.lua_caller = c;
server.lua_time_start = mstime();
server.lua_kill = 0;
if (server.lua_time_limit > 0 && ldb.active == 0) {
lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000);
delhook = 1;
} else if (ldb.active) {
lua_sethook(server.lua,luaLdbLineHook,LUA_MASKLINE|LUA_MASKCOUNT,100000);
delhook = 1;
}
Copy the code
At this point, everything is ready to execute the script directly by calling the lua_pcall function. After executing, you also delete the hook and save the result to the buffer.
This is the entire script execution process, after which Redis also handles some script synchronization issues. We also introduced Redis Lua Scripting Tutorial (Part 1).
conclusion
This concludes the Redis Lua script series. Although the end of the article, but learning is far from over. If you have any questions, please join me. Learn together and make progress together
If you are interested in Lua, please read “Programming in Lua”. If you have conditions, try to support the legal version. If you want to see the quality of Lua first, you can reply to Lua in the background of my public account to obtain e-books.