summaryrefslogtreecommitdiff
path: root/src/function_lua.c
diff options
context:
space:
mode:
authorMeir Shpilraien (Spielrein) <meir@redis.com>2022-01-14 14:02:02 +0200
committerGitHub <noreply@github.com>2022-01-14 14:02:02 +0200
commit4db4b434175b519e2e5a78f2d33a7627c483c367 (patch)
treeb78a8c6c24e3d3dfba8f2c3ae85b6652a690f496 /src/function_lua.c
parent38a511672833037f83669f8f0dc681a7de30bfcb (diff)
downloadredis-4db4b434175b519e2e5a78f2d33a7627c483c367.tar.gz
Function Flags support (no-writes, no-cluster, allow-state, allow-oom) (#10066)
# Redis Functions Flags Following the discussion on #10025 Added Functions Flags support. The PR is divided to 2 sections: * Add named argument support to `redis.register_function` API. * Add support for function flags ## `redis.register_function` named argument support The first part of the PR adds support for named argument on `redis.register_function`, example: ``` redis.register_function{ function_name='f1', callback=function() return 'hello' end, description='some desc' } ``` The positional arguments is also kept, which means that it still possible to write: ``` redis.register_function('f1', function() return 'hello' end) ``` But notice that it is no longer possible to pass the optional description argument on the positional argument version. Positional argument was change to allow passing only the mandatory arguments (function name and callback). To pass more arguments the user must use the named argument version. As with positional arguments, the `function_name` and `callback` is mandatory and an error will be raise if those are missing. Also, an error will be raise if an unknown argument name is given or the arguments type is wrong. Tests was added to verify the new syntax. ## Functions Flags The second part of the PR is adding functions flags support. Flags are given to Redis when the engine calls `functionLibCreateFunction`, supported flags are: * `no-writes` - indicating the function perform no writes which means that it is OK to run it on: * read-only replica * Using FCALL_RO * If disk error detected It will not be possible to run a function in those situations unless the function turns on the `no-writes` flag * `allow-oom` - indicate that its OK to run the function even if Redis is in OOM state, if the function will not turn on this flag it will not be possible to run it if OOM reached (even if the function declares `no-writes` and even if `fcall_ro` is used). If this flag is set, any command will be allow on OOM (even those that is marked with CMD_DENYOOM). The assumption is that this flag is for advance users that knows its meaning and understand what they are doing, and Redis trust them to not increase the memory usage. (e.g. it could be an INCR or a modification on an existing key, or a DEL command) * `allow-state` - indicate that its OK to run the function on stale replica, in this case we will also make sure the function is only perform `stale` commands and raise an error if not. * `no-cluster` - indicate to disallow running the function if cluster is enabled. Default behaviure of functions (if no flags is given): 1. Allow functions to read and write 2. Do not run functions on OOM 3. Do not run functions on stale replica 4. Allow functions on cluster ### Lua API for functions flags On Lua engine, it is possible to give functions flags as `flags` named argument: ``` redis.register_function{function_name='f1', callback=function() return 1 end, flags={'no-writes', 'allow-oom'}, description='description'} ``` The function flags argument must be a Lua table that contains all the requested flags, The following will result in an error: * Unknown flag * Wrong flag type Default behaviour is the same as if no flags are used. Tests were added to verify all flags functionality ## Additional changes * mark FCALL and FCALL_RO with CMD_STALE flag (unlike EVAL), so that they can run if the function was registered with the `allow-stale` flag. * Verify `CMD_STALE` on `scriptCall` (`redis.call`), so it will not be possible to call commands from script while stale unless the command is marked with the `CMD_STALE` flags. so that even if the function is allowed while stale we do not allow it to bypass the `CMD_STALE` flag of commands. * Flags section was added to `FUNCTION LIST` command to provide the set of flags for each function: ``` > FUNCTION list withcode 1) 1) "library_name" 2) "test" 3) "engine" 4) "LUA" 5) "description" 6) (nil) 7) "functions" 8) 1) 1) "name" 2) "f1" 3) "description" 4) (nil) 5) "flags" 6) (empty array) 9) "library_code" 10) "redis.register_function{function_name='f1', callback=function() return 1 end}" ``` * Added API to get Redis version from within a script, The redis version can be provided using: 1. `redis.REDIS_VERSION` - string representation of the redis version in the format of MAJOR.MINOR.PATH 2. `redis.REDIS_VERSION_NUM` - number representation of the redis version in the format of `0x00MMmmpp` (`MM` - major, `mm` - minor, `pp` - patch). The number version can be used to check if version is greater or less another version. The string version can be used to return to the user or print as logs. This new API is provided to eval scripts and functions, it also possible to use this API during functions loading phase.
Diffstat (limited to 'src/function_lua.c')
-rw-r--r--src/function_lua.c234
1 files changed, 200 insertions, 34 deletions
diff --git a/src/function_lua.c b/src/function_lua.c
index e6b8a2727..3dbc8419e 100644
--- a/src/function_lua.c
+++ b/src/function_lua.c
@@ -68,6 +68,13 @@ typedef struct loadCtx {
monotime start_time;
} loadCtx;
+typedef struct registerFunctionArgs {
+ sds name;
+ sds desc;
+ luaFunctionCtx *lua_f_ctx;
+ uint64_t f_flags;
+} registerFunctionArgs;
+
/* Hook for FUNCTION LOAD execution.
* Used to cancel the execution in case of a timeout (500ms).
* This execution should be fast and should only register
@@ -224,56 +231,214 @@ static void luaEngineFreeFunction(void *engine_ctx, void *compiled_function) {
zfree(f_ctx);
}
-static int luaRegisterFunction(lua_State *lua) {
- int argc = lua_gettop(lua);
- if (argc < 2 || argc > 3) {
- luaPushError(lua, "wrong number of arguments to redis.register_function");
- return luaRaiseError(lua);
+static void luaRegisterFunctionArgsInitialize(registerFunctionArgs *register_f_args,
+ sds name,
+ sds desc,
+ luaFunctionCtx *lua_f_ctx,
+ uint64_t flags)
+{
+ *register_f_args = (registerFunctionArgs){
+ .name = name,
+ .desc = desc,
+ .lua_f_ctx = lua_f_ctx,
+ .f_flags = flags,
+ };
+}
+
+static void luaRegisterFunctionArgsDispose(lua_State *lua, registerFunctionArgs *register_f_args) {
+ sdsfree(register_f_args->name);
+ if (register_f_args->desc) sdsfree(register_f_args->desc);
+ lua_unref(lua, register_f_args->lua_f_ctx->lua_function_ref);
+ zfree(register_f_args->lua_f_ctx);
+}
+
+/* Read function flags located on the top of the Lua stack.
+ * On success, return C_OK and set the flags to 'flags' out parameter
+ * Return C_ERR if encounter an unknown flag. */
+static int luaRegisterFunctionReadFlags(lua_State *lua, uint64_t *flags) {
+ int j = 1;
+ int ret = C_ERR;
+ int f_flags = 0;
+ while(1) {
+ lua_pushnumber(lua,j++);
+ lua_gettable(lua,-2);
+ int t = lua_type(lua,-1);
+ if (t == LUA_TNIL) {
+ lua_pop(lua,1);
+ break;
+ }
+ if (!lua_isstring(lua, -1)) {
+ lua_pop(lua,1);
+ goto done;
+ }
+
+ const char *flag_str = lua_tostring(lua, -1);
+ int found = 0;
+ for (scriptFlag *flag = scripts_flags_def; flag->str ; ++flag) {
+ if (!strcasecmp(flag->str, flag_str)) {
+ f_flags |= flag->flag;
+ found = 1;
+ break;
+ }
+ }
+ /* pops the value to continue the iteration */
+ lua_pop(lua,1);
+ if (!found) {
+ /* flag not found */
+ goto done;
+ }
}
- loadCtx *load_ctx = luaGetFromRegistry(lua, REGISTRY_LOAD_CTX_NAME);
- if (!load_ctx) {
- luaPushError(lua, "redis.register_function can only be called on FUNCTION LOAD command");
- return luaRaiseError(lua);
+
+ *flags = f_flags;
+ ret = C_OK;
+
+done:
+ return ret;
+}
+
+static int luaRegisterFunctionReadNamedArgs(lua_State *lua, registerFunctionArgs *register_f_args) {
+ char *err = NULL;
+ sds name = NULL;
+ sds desc = NULL;
+ luaFunctionCtx *lua_f_ctx = NULL;
+ uint64_t flags = 0;
+ if (!lua_istable(lua, 1)) {
+ err = "calling redis.register_function with a single argument is only applicable to Lua table (representing named arguments).";
+ goto error;
}
- if (!lua_isstring(lua, 1)) {
- luaPushError(lua, "first argument to redis.register_function must be a string");
- return luaRaiseError(lua);
+ /* Iterating on all the named arguments */
+ lua_pushnil(lua);
+ while (lua_next(lua, -2)) {
+ /* Stack now: table, key, value */
+ if (!lua_isstring(lua, -2)) {
+ err = "named argument key given to redis.register_function is not a string";
+ goto error;
+ }
+ const char *key = lua_tostring(lua, -2);
+ if (!strcasecmp(key, "function_name")) {
+ if (!(name = luaGetStringSds(lua, -1))) {
+ err = "function_name argument given to redis.register_function must be a string";
+ goto error;
+ }
+ } else if (!strcasecmp(key, "description")) {
+ if (!(desc = luaGetStringSds(lua, -1))) {
+ err = "description argument given to redis.register_function must be a string";
+ goto error;
+ }
+ } else if (!strcasecmp(key, "callback")) {
+ if (!lua_isfunction(lua, -1)) {
+ err = "callback argument given to redis.register_function must be a function";
+ goto error;
+ }
+ int lua_function_ref = luaL_ref(lua, LUA_REGISTRYINDEX);
+
+ lua_f_ctx = zmalloc(sizeof(*lua_f_ctx));
+ lua_f_ctx->lua_function_ref = lua_function_ref;
+ continue; /* value was already popped, so no need to pop it out. */
+ } else if (!strcasecmp(key, "flags")) {
+ if (!lua_istable(lua, -1)) {
+ err = "flags argument to redis.register_function must be a table representing function flags";
+ goto error;
+ }
+ if (luaRegisterFunctionReadFlags(lua, &flags) != C_OK) {
+ err = "unknown flag given";
+ goto error;
+ }
+ } else {
+ /* unknown argument was given, raise an error */
+ err = "unknown argument given to redis.register_function";
+ goto error;
+ }
+ lua_pop(lua, 1); /* pop the value to continue the iteration */
}
- if (!lua_isfunction(lua, 2)) {
- luaPushError(lua, "second argument to redis.register_function must be a function");
- return luaRaiseError(lua);
+ if (!name) {
+ err = "redis.register_function must get a function name argument";
+ goto error;
}
- if (argc == 3 && !lua_isstring(lua, 3)) {
- luaPushError(lua, "third argument to redis.register_function must be a string");
- return luaRaiseError(lua);
+ if (!lua_f_ctx) {
+ err = "redis.register_function must get a callback argument";
+ goto error;
+ }
+
+ luaRegisterFunctionArgsInitialize(register_f_args, name, desc, lua_f_ctx, flags);
+
+ return C_OK;
+
+error:
+ if (name) sdsfree(name);
+ if (desc) sdsfree(desc);
+ if (lua_f_ctx) {
+ lua_unref(lua, lua_f_ctx->lua_function_ref);
+ zfree(lua_f_ctx);
}
+ luaPushError(lua, err);
+ return C_ERR;
+}
- size_t function_name_len;
- const char *function_name = lua_tolstring(lua, 1, &function_name_len);
- sds function_name_sds = sdsnewlen(function_name, function_name_len);
+static int luaRegisterFunctionReadPositionalArgs(lua_State *lua, registerFunctionArgs *register_f_args) {
+ char *err = NULL;
+ sds name = NULL;
+ sds desc = NULL;
+ luaFunctionCtx *lua_f_ctx = NULL;
+ if (!(name = luaGetStringSds(lua, 1))) {
+ err = "first argument to redis.register_function must be a string";
+ goto error;
+ }
- sds desc_sds = NULL;
- if (argc == 3){
- size_t desc_len;
- const char *desc = lua_tolstring(lua, 3, &desc_len);
- desc_sds = sdsnewlen(desc, desc_len);
- lua_pop(lua, 1); /* pop out the description */
+ if (!lua_isfunction(lua, 2)) {
+ err = "second argument to redis.register_function must be a function";
+ goto error;
}
int lua_function_ref = luaL_ref(lua, LUA_REGISTRYINDEX);
- luaFunctionCtx *lua_f_ctx = zmalloc(sizeof(*lua_f_ctx));
- *lua_f_ctx = (luaFunctionCtx ) { .lua_function_ref = lua_function_ref, };
+ lua_f_ctx = zmalloc(sizeof(*lua_f_ctx));
+ lua_f_ctx->lua_function_ref = lua_function_ref;
+
+ luaRegisterFunctionArgsInitialize(register_f_args, name, NULL, lua_f_ctx, 0);
+
+ return C_OK;
+
+error:
+ if (name) sdsfree(name);
+ if (desc) sdsfree(desc);
+ luaPushError(lua, err);
+ return C_ERR;
+}
+
+static int luaRegisterFunctionReadArgs(lua_State *lua, registerFunctionArgs *register_f_args) {
+ int argc = lua_gettop(lua);
+ if (argc < 1 || argc > 2) {
+ luaPushError(lua, "wrong number of arguments to redis.register_function");
+ return C_ERR;
+ }
+
+ if (argc == 1) {
+ return luaRegisterFunctionReadNamedArgs(lua, register_f_args);
+ } else {
+ return luaRegisterFunctionReadPositionalArgs(lua, register_f_args);
+ }
+}
+
+static int luaRegisterFunction(lua_State *lua) {
+ registerFunctionArgs register_f_args = {0};
+
+ loadCtx *load_ctx = luaGetFromRegistry(lua, REGISTRY_LOAD_CTX_NAME);
+ if (!load_ctx) {
+ luaPushError(lua, "redis.register_function can only be called on FUNCTION LOAD command");
+ return luaRaiseError(lua);
+ }
+
+ if (luaRegisterFunctionReadArgs(lua, &register_f_args) != C_OK) {
+ return luaRaiseError(lua);
+ }
sds err = NULL;
- if (functionLibCreateFunction(function_name_sds, lua_f_ctx, load_ctx->li, desc_sds, &err) != C_OK) {
- sdsfree(function_name_sds);
- if (desc_sds) sdsfree(desc_sds);
- lua_unref(lua, lua_f_ctx->lua_function_ref);
- zfree(lua_f_ctx);
+ if (functionLibCreateFunction(register_f_args.name, register_f_args.lua_f_ctx, load_ctx->li, register_f_args.desc, register_f_args.f_flags, &err) != C_OK) {
+ luaRegisterFunctionArgsDispose(lua, &register_f_args);
luaPushError(lua, err);
sdsfree(err);
return luaRaiseError(lua);
@@ -298,6 +463,7 @@ int luaEngineInitEngine() {
lua_settable(lua_engine_ctx->lua, -3);
luaRegisterLogFunction(lua_engine_ctx->lua);
+ luaRegisterVersion(lua_engine_ctx->lua);
lua_settable(lua_engine_ctx->lua, LUA_REGISTRYINDEX);