X-Git-Url: https://git.saurik.com/redis.git/blobdiff_plain/3791000f15ad793854fac48aa4840914970048db..7feb90fa3709a612c272dc1fb0a239752c8c6bbd:/src/scripting.c diff --git a/src/scripting.c b/src/scripting.c index 9269cbf1..794cba5e 100644 --- a/src/scripting.c +++ b/src/scripting.c @@ -4,6 +4,7 @@ #include #include #include +#include char *redisProtocolToLuaType_Int(lua_State *lua, char *reply); char *redisProtocolToLuaType_Bulk(lua_State *lua, char *reply); @@ -67,8 +68,8 @@ char *redisProtocolToLuaType_Bulk(lua_State *lua, char *reply) { long long bulklen; string2ll(reply+1,p-reply-1,&bulklen); - if (bulklen == 0) { - lua_pushnil(lua); + if (bulklen == -1) { + lua_pushboolean(lua,0); return p+2; } else { lua_pushlstring(lua,p+2,bulklen); @@ -79,7 +80,10 @@ char *redisProtocolToLuaType_Bulk(lua_State *lua, char *reply) { char *redisProtocolToLuaType_Status(lua_State *lua, char *reply) { char *p = strchr(reply+1,'\r'); + lua_newtable(lua); + lua_pushstring(lua,"ok"); lua_pushlstring(lua,reply+1,p-reply-1); + lua_settable(lua,-3); return p+2; } @@ -98,23 +102,28 @@ char *redisProtocolToLuaType_MultiBulk(lua_State *lua, char *reply) { long long mbulklen; int j = 0; - printf("--%s-- (%d)\n", reply,(int)(p-reply-1)); string2ll(reply+1,p-reply-1,&mbulklen); p += 2; if (mbulklen == -1) { - lua_pushnil(lua); + lua_pushboolean(lua,0); return p; } - printf("BL: %lld\n", mbulklen); lua_newtable(lua); for (j = 0; j < mbulklen; j++) { - lua_pushnumber(lua,j); + lua_pushnumber(lua,j+1); p = redisProtocolToLuaType(lua,p); lua_settable(lua,-3); } return p; } +void luaPushError(lua_State *lua, char *error) { + lua_newtable(lua); + lua_pushstring(lua,"err"); + lua_pushstring(lua, error); + lua_settable(lua,-3); +} + int luaRedisCommand(lua_State *lua) { int j, argc = lua_gettop(lua); struct redisCommand *cmd; @@ -124,9 +133,26 @@ int luaRedisCommand(lua_State *lua) { /* Build the arguments vector */ argv = zmalloc(sizeof(robj*)*argc); - for (j = 0; j < argc; j++) + for (j = 0; j < argc; j++) { + if (!lua_isstring(lua,j+1)) break; argv[j] = createStringObject((char*)lua_tostring(lua,j+1), lua_strlen(lua,j+1)); + } + + /* Check if one of the arguments passed by the Lua script + * is not a string or an integer (lua_isstring() return true for + * integers as well). */ + if (j != argc) { + j--; + while (j >= 0) { + decrRefCount(argv[j]); + j--; + } + zfree(argv); + luaPushError(lua, + "Lua redis() command arguments must be strings or integers"); + return 1; + } /* Command lookup */ cmd = lookupCommand(argv[0]->ptr); @@ -135,14 +161,11 @@ int luaRedisCommand(lua_State *lua) { { for (j = 0; j < argc; j++) decrRefCount(argv[j]); zfree(argv); - lua_newtable(lua); - lua_pushstring(lua,"err"); if (cmd) - lua_pushstring(lua, + luaPushError(lua, "Wrong number of args calling Redis command From Lua script"); else - lua_pushstring(lua,"Unknown Redis command called from Lua script"); - lua_settable(lua,-3); + luaPushError(lua,"Unknown Redis command called from Lua script"); return 1; } @@ -162,7 +185,7 @@ int luaRedisCommand(lua_State *lua) { while(listLength(c->reply)) { robj *o = listNodeValue(listFirst(c->reply)); - sdscatlen(reply,o->ptr,sdslen(o->ptr)); + reply = sdscatlen(reply,o->ptr,sdslen(o->ptr)); listDelNode(c->reply,listFirst(c->reply)); } redisProtocolToLuaType(lua,reply); @@ -177,12 +200,92 @@ int luaRedisCommand(lua_State *lua) { return 1; } +int luaLogCommand(lua_State *lua) { + int j, argc = lua_gettop(lua); + int level; + sds log; + + if (argc < 2) { + luaPushError(lua, "redis.log() requires two arguments or more."); + return 1; + } else if (!lua_isnumber(lua,-argc)) { + luaPushError(lua, "First argument must be a number (log level)."); + return 1; + } + level = lua_tonumber(lua,-argc); + if (level < REDIS_DEBUG || level > REDIS_WARNING) { + luaPushError(lua, "Invalid debug level."); + return 1; + } + + /* Glue together all the arguments */ + log = sdsempty(); + for (j = 1; j < argc; j++) { + size_t len; + char *s; + + s = (char*)lua_tolstring(lua,(-argc)+j,&len); + if (s) { + if (j != 1) log = sdscatlen(log," ",1); + log = sdscatlen(log,s,len); + } + } + redisLogRaw(level,log); + sdsfree(log); + return 0; +} + +void luaMaskCountHook(lua_State *lua, lua_Debug *ar) { + long long elapsed; + REDIS_NOTUSED(ar); + + elapsed = (ustime()/1000) - server.lua_time_start; + if (elapsed >= server.lua_time_limit) { + redisLog(REDIS_NOTICE,"Lua script aborted for max execution time after %lld milliseconds of running time.",elapsed); + lua_pushstring(lua,"Script aborted for max execution time."); + lua_error(lua); + } +} + void scriptingInit(void) { lua_State *lua = lua_open(); luaL_openlibs(lua); - /* Register the 'r' command */ + /* Initialize a dictionary we use to map SHAs to scripts. + * This is useful for replication, as we need to replicate EVALSHA + * as EVAL, so we need to remember the associated script. */ + server.lua_scripts = dictCreate(&dbDictType,NULL); + + /* Register the redis commands table and fields */ + lua_newtable(lua); + + /* redis.call */ + lua_pushstring(lua,"call"); lua_pushcfunction(lua,luaRedisCommand); + lua_settable(lua,-3); + + /* redis.log and log levels. */ + lua_pushstring(lua,"log"); + lua_pushcfunction(lua,luaLogCommand); + lua_settable(lua,-3); + + lua_pushstring(lua,"LOG_DEBUG"); + lua_pushnumber(lua,REDIS_DEBUG); + lua_settable(lua,-3); + + lua_pushstring(lua,"LOG_VERBOSE"); + lua_pushnumber(lua,REDIS_VERBOSE); + lua_settable(lua,-3); + + lua_pushstring(lua,"LOG_NOTICE"); + lua_pushnumber(lua,REDIS_NOTICE); + lua_settable(lua,-3); + + lua_pushstring(lua,"LOG_WARNING"); + lua_pushnumber(lua,REDIS_WARNING); + lua_settable(lua,-3); + + /* Finally set the table as 'redis' global var. */ lua_setglobal(lua,"redis"); /* Create the (non connected) client that we use to execute Redis commands @@ -214,33 +317,49 @@ void hashScript(char *digest, char *script, size_t len) { } void luaReplyToRedisReply(redisClient *c, lua_State *lua) { - int t = lua_type(lua,1); + int t = lua_type(lua,-1); switch(t) { case LUA_TSTRING: - addReplyBulkCBuffer(c,(char*)lua_tostring(lua,1),lua_strlen(lua,1)); + addReplyBulkCBuffer(c,(char*)lua_tostring(lua,-1),lua_strlen(lua,-1)); break; case LUA_TBOOLEAN: - addReply(c,lua_toboolean(lua,1) ? shared.cone : shared.czero); + addReply(c,lua_toboolean(lua,-1) ? shared.cone : shared.nullbulk); break; case LUA_TNUMBER: - addReplyLongLong(c,(long long)lua_tonumber(lua,1)); + addReplyLongLong(c,(long long)lua_tonumber(lua,-1)); break; case LUA_TTABLE: - /* We need to check if it is an array or an error. - * Error are returned as a single element table with 'err' field. */ + /* We need to check if it is an array, an error, or a status reply. + * Error are returned as a single element table with 'err' field. + * Status replies are returned as single elment table with 'ok' field */ lua_pushstring(lua,"err"); lua_gettable(lua,-2); t = lua_type(lua,-1); if (t == LUA_TSTRING) { - addReplySds(c,sdscatprintf(sdsempty(), - "-%s\r\n",(char*)lua_tostring(lua,-1))); + sds err = sdsnew(lua_tostring(lua,-1)); + sdsmapchars(err,"\r\n"," ",2); + addReplySds(c,sdscatprintf(sdsempty(),"-%s\r\n",err)); + sdsfree(err); + lua_pop(lua,2); + return; + } + + lua_pop(lua,1); + lua_pushstring(lua,"ok"); + lua_gettable(lua,-2); + t = lua_type(lua,-1); + if (t == LUA_TSTRING) { + sds ok = sdsnew(lua_tostring(lua,-1)); + sdsmapchars(ok,"\r\n"," ",2); + addReplySds(c,sdscatprintf(sdsempty(),"+%s\r\n",ok)); + sdsfree(ok); lua_pop(lua,1); } else { void *replylen = addDeferredMultiBulkLength(c); int j = 1, mbulklen = 0; - lua_pop(lua,1); /* Discard the 'err' field value we popped */ + lua_pop(lua,1); /* Discard the 'ok' field value we popped */ while(1) { lua_pushnumber(lua,j++); lua_gettable(lua,-2); @@ -248,17 +367,9 @@ void luaReplyToRedisReply(redisClient *c, lua_State *lua) { if (t == LUA_TNIL) { lua_pop(lua,1); break; - } else if (t == LUA_TSTRING) { - size_t len; - char *s = (char*) lua_tolstring(lua,-1,&len); - - addReplyBulkCBuffer(c,s,len); - mbulklen++; - } else if (t == LUA_TNUMBER) { - addReplyLongLong(c,(long long)lua_tonumber(lua,-1)); - mbulklen++; } - lua_pop(lua,1); + luaReplyToRedisReply(c, lua); + mbulklen++; } setDeferredMultiBulkLength(c,replylen,mbulklen); } @@ -282,7 +393,7 @@ void luaSetGlobalArray(lua_State *lua, char *var, robj **elev, int elec) { lua_setglobal(lua,var); } -void evalCommand(redisClient *c) { +void evalGenericCommand(redisClient *c, int evalsha) { lua_State *lua = server.lua; char funcname[43]; long long numkeys; @@ -299,11 +410,32 @@ void evalCommand(redisClient *c) { * defined into the Lua state */ funcname[0] = 'f'; funcname[1] = '_'; - hashScript(funcname+2,c->argv[1]->ptr,sdslen(c->argv[1]->ptr)); + if (!evalsha) { + /* Hash the code if this is an EVAL call */ + hashScript(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; + + for (j = 0; j < 40; j++) + funcname[j+2] = tolower(sha[j]); + funcname[42] = '\0'; + } + lua_getglobal(lua, funcname); if (lua_isnil(lua,1)) { - /* Function not defined... let's define it. */ - sds funcdef = sdsempty(); + sds funcdef; + + /* Function not defined... let's define it if we have the + * body of the funciton. If this is an EVALSHA call we can just + * return an error. */ + if (evalsha) { + addReply(c, shared.noscripterr); + lua_pop(lua,1); /* remove the nil from the stack */ + return; + } + funcdef = sdsempty(); lua_pop(lua,1); /* remove the nil from the stack */ funcdef = sdscat(funcdef,"function "); @@ -311,7 +443,7 @@ void evalCommand(redisClient *c) { funcdef = sdscatlen(funcdef," ()\n",4); funcdef = sdscatlen(funcdef,c->argv[1]->ptr,sdslen(c->argv[1]->ptr)); funcdef = sdscatlen(funcdef,"\nend\n",5); - printf("Defining:\n%s\n",funcdef); + /* printf("Defining:\n%s\n",funcdef); */ if (luaL_loadbuffer(lua,funcdef,sdslen(funcdef),"func definition")) { addReplyErrorFormat(c,"Error compiling script (new function): %s\n", @@ -328,21 +460,84 @@ void evalCommand(redisClient *c) { return; } lua_getglobal(lua, funcname); + + /* 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, + sdsnewlen(funcname+2,40),c->argv[1]); + redisAssert(retval == DICT_OK); + incrRefCount(c->argv[1]); + } } /* 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); + /* Set an 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 (server.lua_time_limit > 0) { + lua_sethook(lua,luaMaskCountHook,LUA_MASKCOUNT,100000); + server.lua_time_start = ustime()/1000; + } else { + lua_sethook(lua,luaMaskCountHook,0,0); + } + /* At this point whatever this script was never seen before or if it was * already defined, we can call it. We have zero arguments and expect * a single return value. */ if (lua_pcall(lua,0,1,0)) { + selectDb(c,server.lua_client->db->id); /* set DB ID from Lua client */ addReplyErrorFormat(c,"Error running script (call to %s): %s\n", funcname, lua_tostring(lua,-1)); lua_pop(lua,1); + lua_gc(lua,LUA_GCCOLLECT,0); return; } + selectDb(c,server.lua_client->db->id); /* set DB ID from Lua client */ luaReplyToRedisReply(c,lua); + lua_gc(lua,LUA_GCSTEP,1); + + /* If we have slaves attached we want to replicate this command as + * EVAL instead of EVALSHA. We do this also in the AOF as currently there + * is no easy way to propagate a command in a different way in the AOF + * and in the replication link. + * + * IMPROVEMENT POSSIBLE: + * 1) Replicate this command as EVALSHA in the AOF. + * 2) Remember what slave already received a given script, and replicate + * the EVALSHA against this slaves when possible. + */ + if (evalsha) { + robj *script = dictFetchValue(server.lua_scripts,c->argv[1]->ptr); + + redisAssert(script != NULL); + rewriteClientCommandArgument(c,0, + resetRefCount(createStringObject("EVAL",4))); + rewriteClientCommandArgument(c,1,script); + } +} + +void evalCommand(redisClient *c) { + evalGenericCommand(c,0); +} + +void evalShaCommand(redisClient *c) { + if (sdslen(c->argv[1]->ptr) != 40) { + /* We know that a match is not possible if the provided SHA is + * not the right length. So we return an error ASAP, this way + * evalGenericCommand() can be implemented without string length + * sanity check */ + addReply(c, shared.noscripterr); + return; + } + evalGenericCommand(c,1); }