Add scripting persistence and better replication support.
This commit is contained in:
@@ -16,6 +16,21 @@ The 'dbversion' value is stored in the RDB, which makes it incompatible with
|
||||
stock Redis RDB files.
|
||||
|
||||
|
||||
Script Replication
|
||||
------------------
|
||||
|
||||
This version of Redis provides a mechanism to store scripts in RDB files. As
|
||||
a results, scripts are as persistent as the database itself. Also, this
|
||||
allows slave servers to receive scripts when initiating a replication link.
|
||||
|
||||
In addition, Redis behaves a bit differently in order to fully support
|
||||
script replication:
|
||||
- EVAL command with a DB-modifying script will be replicated as-is.
|
||||
- EVAL command with a non-DB modifying script will be replicated as a
|
||||
SCRIPT LOAD command (to save processing on the slave).
|
||||
- The same applies to EVALSHA, which is replicated as an EVAL or SCRIPT LOAD.
|
||||
|
||||
|
||||
Different BGSAVE Types
|
||||
----------------------
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "lzf.h" /* LZF compression library */
|
||||
#include "zipmap.h"
|
||||
#include "endianconv.h"
|
||||
#include "lua.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <sys/types.h>
|
||||
@@ -700,6 +701,20 @@ int rdbSave(char *filename) {
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
}
|
||||
|
||||
/* Store scripts */
|
||||
di = dictGetSafeIterator(server.lua_scripts);
|
||||
while ((de = dictNext(di)) != NULL) {
|
||||
robj key, *o = dictGetVal(de);
|
||||
sds keystr = sdsnew(REDIS_RDB_SCRIPT_KEY_PREFIX);
|
||||
keystr = sdscatsds(keystr, dictGetKey(de));
|
||||
keystr = sdscat(keystr, REDIS_RDB_SCRIPT_KEY_SUFFIX);
|
||||
|
||||
initStaticStringObject(key, keystr);
|
||||
if (rdbSaveKeyValuePair(&rdb,&key,o,-1,-1) == -1) goto werr;
|
||||
}
|
||||
dictReleaseIterator(di);
|
||||
|
||||
di = NULL; /* So that we don't release it again on error. */
|
||||
|
||||
/* EOF opcode */
|
||||
@@ -1190,6 +1205,31 @@ int rdbLoad(char *filename) {
|
||||
if ((key = rdbLoadStringObject(&rdb)) == NULL) goto eoferr;
|
||||
/* Read value */
|
||||
if ((val = rdbLoadObject(type,&rdb)) == NULL) goto eoferr;
|
||||
/* Handle meta-keys */
|
||||
if (val->type == REDIS_STRING && (*(char *)key->ptr & 0xff) == 0xdb) {
|
||||
sds keystr = (sds) key->ptr;
|
||||
|
||||
if (sdslen(keystr) == REDIS_RDB_SCRIPT_KEY_LEN &&
|
||||
!memcmp(keystr, REDIS_RDB_SCRIPT_KEY_PREFIX, sizeof(REDIS_RDB_SCRIPT_KEY_PREFIX)-1)) {
|
||||
char funcname[43];
|
||||
robj *shakey = createStringObject(keystr + sizeof(REDIS_RDB_SCRIPT_KEY_PREFIX)-1, 40);
|
||||
|
||||
funcname[0] = 'f';
|
||||
funcname[1] = '_';
|
||||
memcpy(funcname + 2, shakey->ptr, 40);
|
||||
funcname[42] = 0;
|
||||
|
||||
if (luaCreateFunction(NULL, server.lua, funcname, val) != REDIS_OK) {
|
||||
redisLog(REDIS_WARNING, "FATAL: Data file contains an invalid LUA script.\n");
|
||||
exit(1);
|
||||
}
|
||||
decrRefCount(shakey);
|
||||
decrRefCount(key);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if the key already expired. This function is used when loading
|
||||
* an RDB file from disk, either at startup, or when an RDB was
|
||||
* received from the master. In the latter case, the master is
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
|
||||
/* Special key used to store dbversion in a backward-compatible manner */
|
||||
#define REDIS_RDB_DBVERSION_KEY "\xDB__dbversion__\xDB"
|
||||
#define REDIS_RDB_SCRIPT_KEY_PREFIX "\xDB__lua_script__"
|
||||
#define REDIS_RDB_SCRIPT_KEY_SUFFIX "__\xDB"
|
||||
#define REDIS_RDB_SCRIPT_KEY_LEN (sizeof(REDIS_RDB_SCRIPT_KEY_PREFIX)-1 + sizeof(REDIS_RDB_SCRIPT_KEY_SUFFIX)-1 + 40)
|
||||
|
||||
/* Defines related to the dump file format. To store 32 bits lengths for short
|
||||
* keys requires a lot of space, so we check the most significant 2 bits of
|
||||
|
||||
@@ -1075,6 +1075,7 @@ char *sentinelHandleConfiguration(char **argv, int argc);
|
||||
|
||||
/* Scripting */
|
||||
void scriptingInit(void);
|
||||
int luaCreateFunction(redisClient *c, lua_State *lua, char *funcname, robj *body);
|
||||
|
||||
/* Git SHA1 */
|
||||
char *redisGitSHA1(void);
|
||||
|
||||
+25
-4
@@ -757,16 +757,20 @@ int luaCreateFunction(redisClient *c, lua_State *lua, char *funcname, robj *body
|
||||
funcdef = sdscatlen(funcdef," end",4);
|
||||
|
||||
if (luaL_loadbuffer(lua,funcdef,sdslen(funcdef),"@user_script")) {
|
||||
addReplyErrorFormat(c,"Error compiling script (new function): %s\n",
|
||||
lua_tostring(lua,-1));
|
||||
if (c != NULL) {
|
||||
addReplyErrorFormat(c,"Error compiling script (new function): %s\n",
|
||||
lua_tostring(lua,-1));
|
||||
}
|
||||
lua_pop(lua,1);
|
||||
sdsfree(funcdef);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
sdsfree(funcdef);
|
||||
if (lua_pcall(lua,0,0,0)) {
|
||||
addReplyErrorFormat(c,"Error running script (new function): %s\n",
|
||||
lua_tostring(lua,-1));
|
||||
if (c != NULL) {
|
||||
addReplyErrorFormat(c,"Error running script (new function): %s\n",
|
||||
lua_tostring(lua,-1));
|
||||
}
|
||||
lua_pop(lua,1);
|
||||
return REDIS_ERR;
|
||||
}
|
||||
@@ -911,6 +915,22 @@ void evalGenericCommand(redisClient *c, int evalsha) {
|
||||
resetRefCount(createStringObject("EVAL",4)));
|
||||
rewriteClientCommandArgument(c,1,script);
|
||||
}
|
||||
|
||||
/* When we have slaves we want to always replicate scripts, but avoid
|
||||
* EVAL when a script has no impact on the slave.
|
||||
*/
|
||||
if (!server.lua_write_dirty) {
|
||||
robj *script = createStringObject("SCRIPT", 6);
|
||||
robj *load = createStringObject("LOAD", 4);
|
||||
|
||||
/* Rewrite as SCRIPT LOAD. Note that SHA1 expansion has already
|
||||
* been done here.
|
||||
*/
|
||||
rewriteClientCommandVector(c, 3, script, load, c->argv[1]);
|
||||
decrRefCount(script);
|
||||
decrRefCount(load);
|
||||
server.dirty++;
|
||||
}
|
||||
}
|
||||
|
||||
void evalCommand(redisClient *c) {
|
||||
@@ -1004,6 +1024,7 @@ void scriptCommand(redisClient *c) {
|
||||
}
|
||||
addReplyBulkCBuffer(c,funcname+2,40);
|
||||
sdsfree(sha);
|
||||
server.dirty++; /* Replicate this command */
|
||||
} else if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"kill")) {
|
||||
if (server.lua_caller == NULL) {
|
||||
addReplySds(c,sdsnew("-NOTBUSY No scripts in execution right now.\r\n"));
|
||||
|
||||
@@ -367,3 +367,41 @@ start_server {tags {"scripting repl"}} {
|
||||
} {a 1}
|
||||
}
|
||||
}
|
||||
|
||||
start_server {tags {"persistent script repl"}} {
|
||||
start_server {} {
|
||||
test {Load read-only script 99 into the main instance using EVAL} {
|
||||
r eval {return 99} 0
|
||||
} {99}
|
||||
|
||||
test {Connect a slave to the main 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 slave"
|
||||
}
|
||||
}
|
||||
|
||||
test {EVALSHA against slave to run script 99} {
|
||||
r -1 evalsha a2eba4a6e7d6643bc4b14aa4f9e0eedd55dbae3d 0
|
||||
} {99}
|
||||
|
||||
test {Load read-only script 98 into the main instance using EVAL} {
|
||||
r eval {return 98} 0
|
||||
} {98}
|
||||
|
||||
test {EVALSHA against slave to run script 98} {
|
||||
r -1 evalsha 069e4d5d0c3afa79c9a545d8c620dbb94c2a5996 0
|
||||
} {98}
|
||||
|
||||
test {Load read-only script 97 into the main instance using SCRIPT LOAD} {
|
||||
r script load {return 97}
|
||||
} {3f1673c4bedd6a9bc81500f26a454cd277092f80}
|
||||
|
||||
test {EVALSHA against slave to run script 97} {
|
||||
r -1 evalsha 3f1673c4bedd6a9bc81500f26a454cd277092f80 0
|
||||
} {97}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user