summaryrefslogtreecommitdiff
path: root/src/script_lua.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/script_lua.c')
-rw-r--r--src/script_lua.c251
1 files changed, 195 insertions, 56 deletions
diff --git a/src/script_lua.c b/src/script_lua.c
index 82591d3fc..9a08a7e47 100644
--- a/src/script_lua.c
+++ b/src/script_lua.c
@@ -238,9 +238,12 @@ static void redisProtocolToLuaType_Error(void *ctx, const char *str, size_t len,
* to push elements to the stack. On failure, exit with panic. */
serverPanic("lua stack limit reach when parsing redis.call reply");
}
- lua_newtable(lua);
- lua_pushstring(lua,"err");
- lua_pushlstring(lua,str,len);
+ sds err_msg = sdscatlen(sdsnew("-"), str, len);
+ luaPushErrorBuff(lua,err_msg);
+ /* push a field indicate to ignore updating the stats on this error
+ * because it was already updated when executing the command. */
+ lua_pushstring(lua,"ignore_error_stats_update");
+ lua_pushboolean(lua, true);
lua_settable(lua,-3);
}
@@ -428,46 +431,66 @@ static void redisProtocolToLuaType_Double(void *ctx, double d, const char *proto
/* This function is used in order to push an error on the Lua stack in the
* format used by redis.pcall to return errors, which is a lua table
- * with a single "err" field set to the error string. Note that this
- * table is never a valid reply by proper commands, since the returned
- * tables are otherwise always indexed by integers, never by strings. */
-void luaPushError(lua_State *lua, char *error) {
+ * with an "err" field set to the error string including the error code.
+ * Note that this table is never a valid reply by proper commands,
+ * since the returned tables are otherwise always indexed by integers, never by strings.
+ *
+ * The function takes ownership on the given err_buffer. */
+void luaPushErrorBuff(lua_State *lua, sds err_buffer) {
sds msg;
+ sds error_code;
/* If debugging is active and in step mode, log errors resulting from
* Redis commands. */
if (ldbIsEnabled()) {
- ldbLog(sdscatprintf(sdsempty(),"<error> %s",error));
+ ldbLog(sdscatprintf(sdsempty(),"<error> %s",err_buffer));
}
- lua_newtable(lua);
- lua_pushstring(lua,"err");
-
/* There are two possible formats for the received `error` string:
* 1) "-CODE msg": in this case we remove the leading '-' since we don't store it as part of the lua error format.
* 2) "msg": in this case we prepend a generic 'ERR' code since all error statuses need some error code.
* We support format (1) so this function can reuse the error messages used in other places in redis.
* We support format (2) so it'll be easy to pass descriptive errors to this function without worrying about format.
*/
- if (error[0] == '-')
- msg = sdsnew(error+1);
- else
- msg = sdscatprintf(sdsempty(), "ERR %s", error);
+ if (err_buffer[0] == '-') {
+ /* derive error code from the message */
+ char *err_msg = strstr(err_buffer, " ");
+ if (!err_msg) {
+ msg = sdsnew(err_buffer+1);
+ error_code = sdsnew("ERR");
+ } else {
+ *err_msg = '\0';
+ msg = sdsnew(err_msg+1);
+ error_code = sdsnew(err_buffer + 1);
+ }
+ sdsfree(err_buffer);
+ } else {
+ msg = err_buffer;
+ error_code = sdsnew("ERR");
+ }
/* Trim newline at end of string. If we reuse the ready-made Redis error objects (case 1 above) then we might
* have a newline that needs to be trimmed. In any case the lua Redis error table shouldn't end with a newline. */
msg = sdstrim(msg, "\r\n");
- lua_pushstring(lua, msg);
- sdsfree(msg);
+ sds final_msg = sdscatfmt(error_code, " %s", msg);
+
+ lua_newtable(lua);
+ lua_pushstring(lua,"err");
+ lua_pushstring(lua, final_msg);
lua_settable(lua,-3);
+
+ sdsfree(msg);
+ sdsfree(final_msg);
+}
+
+void luaPushError(lua_State *lua, const char *error) {
+ luaPushErrorBuff(lua, sdsnew(error));
}
/* In case the error set into the Lua stack by luaPushError() was generated
* by the non-error-trapping version of redis.pcall(), which is redis.call(),
* this function will raise the Lua error so that the execution of the
* script will be halted. */
-int luaRaiseError(lua_State *lua) {
- lua_pushstring(lua,"err");
- lua_gettable(lua,-2);
+int luaError(lua_State *lua) {
return lua_error(lua);
}
@@ -517,8 +540,15 @@ static void luaReplyToRedisReply(client *c, client* script_client, lua_State *lu
lua_gettable(lua,-2);
t = lua_type(lua,-1);
if (t == LUA_TSTRING) {
- addReplyErrorFormat(c,"-%s",lua_tostring(lua,-1));
- lua_pop(lua,2);
+ lua_pop(lua, 1); /* pop the error message, we will use luaExtractErrorInformation to get error information */
+ errorInfo err_info = {0};
+ luaExtractErrorInformation(lua, &err_info);
+ addReplyErrorFormatEx(c,
+ err_info.ignore_err_stats_update? ERR_REPLY_FLAG_NO_STATS_UPDATE: 0,
+ "-%s",
+ err_info.msg);
+ luaErrorInformationDiscard(&err_info);
+ lua_pop(lua,1); /* pop the result table */
return;
}
lua_pop(lua,1); /* Discard field name pushed before. */
@@ -719,7 +749,7 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) {
scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
if (!rctx) {
luaPushError(lua, "redis.call/pcall can only be called inside a script invocation");
- return luaRaiseError(lua);
+ return luaError(lua);
}
sds err = NULL;
client* c = rctx->c;
@@ -728,7 +758,7 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) {
int argc;
robj **argv = luaArgsToRedisArgv(lua, &argc);
if (argv == NULL) {
- return raise_error ? luaRaiseError(lua) : 1;
+ return raise_error ? luaError(lua) : 1;
}
static int inuse = 0; /* Recursive calls detection. */
@@ -767,6 +797,11 @@ static int luaRedisGenericCommand(lua_State *lua, int raise_error) {
if (err) {
luaPushError(lua, err);
sdsfree(err);
+ /* push a field indicate to ignore updating the stats on this error
+ * because it was already updated when executing the command. */
+ lua_pushstring(lua,"ignore_error_stats_update");
+ lua_pushboolean(lua, true);
+ lua_settable(lua,-3);
goto cleanup;
}
@@ -811,11 +846,40 @@ cleanup:
/* If we are here we should have an error in the stack, in the
* form of a table with an "err" field. Extract the string to
* return the plain error. */
- return luaRaiseError(lua);
+ return luaError(lua);
}
return 1;
}
+/* Our implementation to lua pcall.
+ * We need this implementation for backward
+ * comparability with older Redis versions.
+ *
+ * On Redis 7, the error object is a table,
+ * compare to older version where the error
+ * object is a string. To keep backward
+ * comparability we catch the table object
+ * and just return the error message. */
+static int luaRedisPcall(lua_State *lua) {
+ int argc = lua_gettop(lua);
+ lua_pushboolean(lua, 1); /* result place holder */
+ lua_insert(lua, 1);
+ if (lua_pcall(lua, argc - 1, LUA_MULTRET, 0)) {
+ /* Error */
+ lua_remove(lua, 1); /* remove the result place holder, now we have room for at least one element */
+ if (lua_istable(lua, -1)) {
+ lua_getfield(lua, -1, "err");
+ if (lua_isstring(lua, -1)) {
+ lua_replace(lua, -2); /* replace the error message with the table */
+ }
+ }
+ lua_pushboolean(lua, 0); /* push result */
+ lua_insert(lua, 1);
+ }
+ return lua_gettop(lua);
+
+}
+
/* redis.call() */
static int luaRedisCallCommand(lua_State *lua) {
return luaRedisGenericCommand(lua,1);
@@ -835,8 +899,8 @@ static int luaRedisSha1hexCommand(lua_State *lua) {
char *s;
if (argc != 1) {
- lua_pushstring(lua, "wrong number of arguments");
- return lua_error(lua);
+ luaPushError(lua, "wrong number of arguments");
+ return luaError(lua);
}
s = (char*)lua_tolstring(lua,1,&len);
@@ -867,7 +931,21 @@ static int luaRedisReturnSingleFieldTable(lua_State *lua, char *field) {
/* redis.error_reply() */
static int luaRedisErrorReplyCommand(lua_State *lua) {
- return luaRedisReturnSingleFieldTable(lua,"err");
+ if (lua_gettop(lua) != 1 || lua_type(lua,-1) != LUA_TSTRING) {
+ luaPushError(lua, "wrong number or type of arguments");
+ return 1;
+ }
+
+ /* add '-' if not exists */
+ const char *err = lua_tostring(lua, -1);
+ sds err_buff = NULL;
+ if (err[0] != '-') {
+ err_buff = sdscatfmt(sdsempty(), "-%s", err);
+ } else {
+ err_buff = sdsnew(err);
+ }
+ luaPushErrorBuff(lua, err_buff);
+ return 1;
}
/* redis.status_reply() */
@@ -884,19 +962,19 @@ static int luaRedisSetReplCommand(lua_State *lua) {
scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
if (!rctx) {
- lua_pushstring(lua, "redis.set_repl can only be called inside a script invocation");
- return lua_error(lua);
+ luaPushError(lua, "redis.set_repl can only be called inside a script invocation");
+ return luaError(lua);
}
if (argc != 1) {
- lua_pushstring(lua, "redis.set_repl() requires two arguments.");
- return lua_error(lua);
+ luaPushError(lua, "redis.set_repl() requires two arguments.");
+ return luaError(lua);
}
flags = lua_tonumber(lua,-1);
if ((flags & ~(PROPAGATE_AOF|PROPAGATE_REPL)) != 0) {
- lua_pushstring(lua, "Invalid replication flags. Use REPL_AOF, REPL_REPLICA, REPL_ALL or REPL_NONE.");
- return lua_error(lua);
+ luaPushError(lua, "Invalid replication flags. Use REPL_AOF, REPL_REPLICA, REPL_ALL or REPL_NONE.");
+ return luaError(lua);
}
scriptSetRepl(rctx, flags);
@@ -909,8 +987,8 @@ static int luaRedisSetReplCommand(lua_State *lua) {
static int luaRedisAclCheckCmdPermissionsCommand(lua_State *lua) {
scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
if (!rctx) {
- lua_pushstring(lua, "redis.acl_check_cmd can only be called inside a script invocation");
- return lua_error(lua);
+ luaPushError(lua, "redis.acl_check_cmd can only be called inside a script invocation");
+ return luaError(lua);
}
int raise_error = 0;
@@ -918,12 +996,12 @@ static int luaRedisAclCheckCmdPermissionsCommand(lua_State *lua) {
robj **argv = luaArgsToRedisArgv(lua, &argc);
/* Require at least one argument */
- if (argv == NULL) return lua_error(lua);
+ if (argv == NULL) return luaError(lua);
/* Find command */
struct redisCommand *cmd;
if ((cmd = lookupCommand(argv, argc)) == NULL) {
- lua_pushstring(lua, "Invalid command passed to redis.acl_check_cmd()");
+ luaPushError(lua, "Invalid command passed to redis.acl_check_cmd()");
raise_error = 1;
} else {
int keyidxptr;
@@ -937,7 +1015,7 @@ static int luaRedisAclCheckCmdPermissionsCommand(lua_State *lua) {
while (argc--) decrRefCount(argv[argc]);
zfree(argv);
if (raise_error)
- return lua_error(lua);
+ return luaError(lua);
else
return 1;
}
@@ -950,16 +1028,16 @@ static int luaLogCommand(lua_State *lua) {
sds log;
if (argc < 2) {
- lua_pushstring(lua, "redis.log() requires two arguments or more.");
- return lua_error(lua);
+ luaPushError(lua, "redis.log() requires two arguments or more.");
+ return luaError(lua);
} else if (!lua_isnumber(lua,-argc)) {
- lua_pushstring(lua, "First argument must be a number (log level).");
- return lua_error(lua);
+ luaPushError(lua, "First argument must be a number (log level).");
+ return luaError(lua);
}
level = lua_tonumber(lua,-argc);
if (level < LL_DEBUG || level > LL_WARNING) {
- lua_pushstring(lua, "Invalid debug level.");
- return lua_error(lua);
+ luaPushError(lua, "Invalid debug level.");
+ return luaError(lua);
}
if (level < server.verbosity) return 0;
@@ -984,20 +1062,20 @@ static int luaLogCommand(lua_State *lua) {
static int luaSetResp(lua_State *lua) {
scriptRunCtx* rctx = luaGetFromRegistry(lua, REGISTRY_RUN_CTX_NAME);
if (!rctx) {
- lua_pushstring(lua, "redis.setresp can only be called inside a script invocation");
- return lua_error(lua);
+ luaPushError(lua, "redis.setresp can only be called inside a script invocation");
+ return luaError(lua);
}
int argc = lua_gettop(lua);
if (argc != 1) {
- lua_pushstring(lua, "redis.setresp() requires one argument.");
- return lua_error(lua);
+ luaPushError(lua, "redis.setresp() requires one argument.");
+ return luaError(lua);
}
int resp = lua_tonumber(lua,-argc);
if (resp != 2 && resp != 3) {
- lua_pushstring(lua, "RESP version must be 2 or 3.");
- return lua_error(lua);
+ luaPushError(lua, "RESP version must be 2 or 3.");
+ return luaError(lua);
}
scriptSetResp(rctx, resp);
return 0;
@@ -1197,6 +1275,9 @@ void luaRegisterRedisAPI(lua_State* lua) {
luaLoadLibraries(lua);
luaRemoveUnsupportedFunctions(lua);
+ lua_pushcfunction(lua,luaRedisPcall);
+ lua_setglobal(lua, "pcall");
+
/* Register the redis commands table and fields */
lua_newtable(lua);
@@ -1357,11 +1438,50 @@ static void luaMaskCountHook(lua_State *lua, lua_Debug *ar) {
*/
lua_sethook(lua, luaMaskCountHook, LUA_MASKLINE, 0);
- lua_pushstring(lua,"Script killed by user with SCRIPT KILL...");
- lua_error(lua);
+ luaPushError(lua,"Script killed by user with SCRIPT KILL...");
+ luaError(lua);
}
}
+void luaErrorInformationDiscard(errorInfo *err_info) {
+ if (err_info->msg) sdsfree(err_info->msg);
+ if (err_info->source) sdsfree(err_info->source);
+ if (err_info->line) sdsfree(err_info->line);
+}
+
+void luaExtractErrorInformation(lua_State *lua, errorInfo *err_info) {
+ if (lua_isstring(lua, -1)) {
+ err_info->msg = sdscatfmt(sdsempty(), "ERR %s", lua_tostring(lua, -1));
+ err_info->line = NULL;
+ err_info->source = NULL;
+ err_info->ignore_err_stats_update = 0;
+ }
+
+ lua_getfield(lua, -1, "err");
+ if (lua_isstring(lua, -1)) {
+ err_info->msg = sdsnew(lua_tostring(lua, -1));
+ }
+ lua_pop(lua, 1);
+
+ lua_getfield(lua, -1, "source");
+ if (lua_isstring(lua, -1)) {
+ err_info->source = sdsnew(lua_tostring(lua, -1));
+ }
+ lua_pop(lua, 1);
+
+ lua_getfield(lua, -1, "line");
+ if (lua_isstring(lua, -1)) {
+ err_info->line = sdsnew(lua_tostring(lua, -1));
+ }
+ lua_pop(lua, 1);
+
+ lua_getfield(lua, -1, "ignore_error_stats_update");
+ if (lua_isboolean(lua, -1)) {
+ err_info->ignore_err_stats_update = lua_toboolean(lua, -1);
+ }
+ lua_pop(lua, 1);
+}
+
void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t nkeys, robj** args, size_t nargs, int debug_enabled) {
client* c = run_ctx->original_client;
int delhook = 0;
@@ -1419,9 +1539,28 @@ void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t
}
if (err) {
- addReplyErrorFormat(c,"Error running script (call to %s): %s\n",
- run_ctx->funcname, lua_tostring(lua,-1));
- lua_pop(lua,1); /* Consume the Lua reply and remove error handler. */
+ /* Error object is a table of the following format:
+ * {err='<error msg>', source='<source file>', line=<line>}
+ * We can construct the error message from this information */
+ if (!lua_istable(lua, -1)) {
+ /* Should not happened, and we should considered assert it */
+ addReplyErrorFormat(c,"Error running script (call to %s)\n", run_ctx->funcname);
+ } else {
+ errorInfo err_info = {0};
+ sds final_msg = sdsempty();
+ luaExtractErrorInformation(lua, &err_info);
+ final_msg = sdscatfmt(final_msg, "-%s",
+ err_info.msg);
+ if (err_info.line && err_info.source) {
+ final_msg = sdscatfmt(final_msg, " script: %s, on %s:%s.",
+ run_ctx->funcname,
+ err_info.source,
+ err_info.line);
+ }
+ addReplyErrorSdsEx(c, final_msg, err_info.ignore_err_stats_update? ERR_REPLY_FLAG_NO_STATS_UPDATE : 0);
+ luaErrorInformationDiscard(&err_info);
+ }
+ lua_pop(lua,1); /* Consume the Lua error */
} else {
/* On success convert the Lua return value into Redis protocol, and
* send it to * the client. */