[PATCH v2 9/9] box: export _func functions with box.func folder

Vladimir Davydov vdavydov.dev at gmail.com
Mon Jun 10 18:18:46 MSK 2019


On Thu, Jun 06, 2019 at 03:04:05PM +0300, Kirill Shcherbatov wrote:
> Closes #4182
> Needed for #1260
> 
> @TarantoolBot document
> Title: New function machinery
> 
> This patchset introduces a set of changes in Tarantool function
> machinery.
> 1. At first, the format of _func space is changed. A new space
> format is
> [<id> UINT, <owner> UINT, <name> STR, <setuid> UINT,
>  <language> STR, <body> STR, <returns> STR,
>  <is_deterministic> BOOL, <opts> MAP]
> 
> and box.schema.func.create endpoint is changed correspondingly:
> 
> box.schema.func.create('funcname', <setuid = true|FALSE>,
> 		<if_not_exists = true|FALSE>, <language = LUA|c>,
> 		<body = string ('')>, <returns = string (ANY)>,
> 		<is_deterministic = true|FALSE>,
> 		<opts = { <is_sandboxed = true|FALSE>} >)
> 
> 2. Now all registered with box.schema.func.create functions are
> exported in box.func folder.
> Each function have :call and :drop method. The :drop method
> just a shortcut for legacy box.schema.func.drop interface.

I wouldn't call box.schema.func.drop "legacy".

> The :call method is similar to net.box connection:call method
> except the format or returned value: the func:call method
> returns a table of values like space:select does.

Hmm, why is that? I'd expect 'call' to return exactly what the function
returns.

> 
> 3. This patchset also introduces 'persistent' Lua functions.
> Such functions are stored in snapshoot and are available after
> restart.
> To create a persistent Lua function, specify function body
> in box.schema.func.create call:
> e.g. body = "function(a, b) return a + b end"
> 
> 4. A Lua persistent function may be 'sandboxed'. The 'sandboxed'
> function executed in isolated environment:
>   a. only limited set of Lua functions and modules are available:
>     -assert -error -pairs -ipairs -next -pcall -xpcall -type
>     -print -select -string -tonumber -tostring -unpack -math -utf8;
>   b. global variables are forbidden
> 
> Example:
> lua_code = [[function(a, b) return a + b end]]
> box.schema.func.create('sum', {body = lua_code,
> 		is_deterministic = true, returns = 'integer',
> 		opts = {is_sandboxed = true}})
> tarantool> box.func.sum
> ---
> - is_sandboxed: true
>   returns: integer
>   is_deterministic: true
>   id: 2
>   language: LUA
>   name: sum
>   is_persistent: true
>   setuid: false
> ...
> box.func.sum:call({1, 2})
> ---
> - - 3
> ...
> conn:call("sum", {1, 3})
> ---
> - 3
> ...
> ---

Please split the docbot request in two:

 1. box.func
 2. persistent functions

Each should go to the corresponding commit.

>  src/box/alter.cc                  |   6 +-
>  src/box/func.c                    | 138 ++++++++++++++++++++++++++++++
>  src/box/func.h                    |   5 ++
>  src/box/lua/call.c                |  22 +++++
>  src/box/lua/init.c                |   2 +
>  src/box/lua/schema.lua            |  25 ++++++
>  src/box/schema.cc                 |   1 +
>  src/box/schema.h                  |  16 ++--
>  test/box/misc.result              |   1 +
>  test/box/persistent_func.result   |  85 ++++++++++++++++++
>  test/box/persistent_func.test.lua |  36 ++++++++
>  11 files changed, 331 insertions(+), 6 deletions(-)
> 
> diff --git a/src/box/alter.cc b/src/box/alter.cc
> index c769e4f3d..79477aabf 100644
> --- a/src/box/alter.cc
> +++ b/src/box/alter.cc
> @@ -2517,7 +2517,9 @@ static void
>  func_cache_remove_func(struct trigger *trigger, void * /* event */)
>  {
>  	struct func *old_func = (struct func *) trigger->data;
> -	func_cache_delete(old_func->def->fid);
> +	uint32_t fid = old_func->def->fid;
> +	func_cache_delete(fid);
> +	trigger_run_xc(&on_alter_func, (void *)(uintptr_t)fid);

Ouch. Wouldn't it be better to pass the func pointer instead, similarly
to how on_alter_space does?

>  	func_delete(old_func);
>  }
>  
> @@ -2530,6 +2532,7 @@ func_cache_replace_func(struct trigger *trigger, void * /* event */)
>  	func_cache_replace(new_func, &old_func);
>  	assert(old_func != NULL);
>  	func_delete(old_func);
> +	trigger_run_xc(&on_alter_func, (void *)(uintptr_t)new_func->def->fid);
>  }
>  
>  /**
> @@ -2562,6 +2565,7 @@ on_replace_dd_func(struct trigger * /* trigger */, void *event)
>  		func_cache_replace(func, &old_func);
>  		assert(old_func == NULL);
>  		func_guard.is_active = false;
> +		trigger_run_xc(&on_alter_func, (void *)(uintptr_t)fid);
>  		struct trigger *on_rollback =
>  			txn_alter_trigger_new(func_cache_remove_func, func);
>  		txn_on_rollback(txn, on_rollback);
> diff --git a/src/box/func.c b/src/box/func.c
> index f2065422c..fee7a6aed 100644
> --- a/src/box/func.c
> +++ b/src/box/func.c
> @@ -28,11 +28,13 @@
>   * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
>   * SUCH DAMAGE.
>   */
> +#include "fiber.h"
>  #include "func.h"
>  #include "trivia/config.h"
>  #include "assoc.h"
>  #include "session.h"
>  #include "lua/msgpack.h"
> +#include "lua/trigger.h"
>  #include "lua/utils.h"
>  #include "schema.h"
>  #include "txn.h"
> @@ -830,3 +832,139 @@ box_func_execute_by_name(const char *name, uint32_t name_len, struct port *port,
>  	assert(rc == 0 || diag_last_error(&fiber()->diag) != NULL);
>  	return rc;
>  }
> +
> +static void
> +lbox_func_new(struct lua_State *L, struct func *func)
> +{
> +	lua_getfield(L, LUA_GLOBALSINDEX, "box");
> +	lua_getfield(L, -1, "func");
> +	if (!lua_istable(L, -1)) {
> +		lua_pop(L, 1); /* pop nil */
> +		lua_newtable(L);
> +		lua_setfield(L, -2, "func");
> +		lua_getfield(L, -1, "func");
> +	}
> +	lua_rawgeti(L, -1, func->def->fid);
> +	if (lua_isnil(L, -1)) {
> +		/*
> +		 * If the function already exists, modify it,
> +		 * rather than create a new one -- to not
> +		 * invalidate Lua variable references to old func
> +		 * outside the box.schema.func[].
> +		 */
> +		lua_pop(L, 1);
> +		lua_newtable(L);
> +		lua_rawseti(L, -2, func->def->fid);
> +		lua_rawgeti(L, -1, func->def->fid);
> +	} else {
> +		/* Clear the reference to old space by old name. */

space?

> +		lua_getfield(L, -1, "name");
> +		lua_pushnil(L);
> +		lua_settable(L, -4);
> +	}
> +
> +	int top = lua_gettop(L);
> +	lua_pushstring(L, "id");
> +	lua_pushnumber(L, func->def->fid);
> +	lua_settable(L, top);
> +
> +	lua_pushstring(L, "name");
> +	lua_pushstring(L, func->def->name);
> +	lua_settable(L, top);
> +
> +	lua_pushstring(L, "is_persistent");
> +	lua_pushboolean(L, func_def_is_persistent(func->def));
> +	lua_settable(L, top);
> +
> +	lua_pushstring(L, "setuid");
> +	lua_pushboolean(L, func->def->setuid);
> +	lua_settable(L, top);
> +
> +	lua_pushstring(L, "is_deterministic");
> +	lua_pushboolean(L, func->def->is_deterministic);
> +	lua_settable(L, top);
> +
> +	lua_pushstring(L, "language");
> +	lua_pushstring(L, func_language_strs[func->def->language]);
> +	lua_settable(L, top);
> +
> +	lua_pushstring(L, "returns");
> +	lua_pushstring(L, field_type_strs[func->def->returns]);
> +	lua_settable(L, top);
> +
> +	if (func->def->opts.is_sandboxed) {
> +		lua_pushstring(L, "is_sandboxed");
> +		lua_pushboolean(L, func->def->opts.is_sandboxed);
> +		lua_settable(L, top);
> +	}
> +
> +	/* Bless func object. */
> +	lua_getfield(L, LUA_GLOBALSINDEX, "box");
> +	lua_pushstring(L, "schema");
> +	lua_gettable(L, -2);
> +	lua_pushstring(L, "func");
> +	lua_gettable(L, -2);
> +	lua_pushstring(L, "bless");
> +	lua_gettable(L, -2);
> +
> +	lua_pushvalue(L, top);
> +	lua_call(L, 1, 0);
> +	lua_pop(L, 3);
> +
> +	lua_setfield(L, -2, func->def->name);
> +
> +	lua_pop(L, 2);
> +}
> +
> +static void
> +lbox_func_delete(struct lua_State *L, uint32_t fid)
> +{
> +	lua_getfield(L, LUA_GLOBALSINDEX, "box");
> +	lua_getfield(L, -1, "func");
> +	lua_rawgeti(L, -1, fid);
> +	if (!lua_isnil(L, -1)) {
> +		lua_getfield(L, -1, "name");
> +		lua_pushnil(L);
> +		lua_rawset(L, -4);
> +		lua_pop(L, 1); /* pop func */
> +
> +		lua_pushnil(L);
> +		lua_rawseti(L, -2, fid);
> +	} else {
> +		lua_pop(L, 1);
> +	}
> +	lua_pop(L, 2); /* box, func */
> +}
> +
> +static void
> +lbox_func_new_or_delete(struct trigger *trigger, void *event)
> +{
> +	struct lua_State *L = (struct lua_State *) trigger->data;
> +	uint32_t fid = (uint32_t)(uintptr_t)event;
> +	struct func *func = func_by_id(fid);
> +	/* Export only persistent Lua functions. */
> +	if (func != NULL)
> +		lbox_func_new(L, func);
> +	else
> +		lbox_func_delete(L, fid);
> +}
> +
> +static struct trigger on_alter_func_in_lua = {
> +	RLIST_LINK_INITIALIZER, lbox_func_new_or_delete, NULL, NULL
> +};
> +
> +void
> +box_lua_func_init(struct lua_State *L)
> +{
> +	/*
> +	 * Register the trigger that will push persistent
> +	 * Lua functions objects to Lua.
> +	 */
> +	on_alter_func_in_lua.data = L;
> +	trigger_add(&on_alter_func, &on_alter_func_in_lua);
> +	static const struct luaL_Reg funclib [] = {
> +		{NULL, NULL}
> +	};
> +	luaL_register_module(L, "box.func", funclib);
> +	lua_pop(L, 1);

Hmm, is it really necessary to register a new module from C?

> +}
> diff --git a/src/box/func.h b/src/box/func.h
> index 7b920d7d3..89f503eea 100644
> --- a/src/box/func.h
> +++ b/src/box/func.h
> @@ -44,6 +44,7 @@ extern "C" {
>  
>  struct port;
>  struct func_vtab;
> +struct lua_State;
>  
>  /**
>   * Dynamic shared module.
> @@ -160,6 +161,10 @@ int
>  box_func_execute_by_name(const char *name, uint32_t name_len, struct port *port,
>  			 const char *args, const char *args_end);
>  
> +/** Initialize Lua export trigger for _func objects. */
> +void
> +box_lua_func_init(struct lua_State *L);
> +
>  #if defined(__cplusplus)
>  } /* extern "C" */
>  #endif /* defined(__cplusplus) */
> diff --git a/src/box/lua/call.c b/src/box/lua/call.c
> index e40c9a3f7..f85e1fc30 100644
> --- a/src/box/lua/call.c
> +++ b/src/box/lua/call.c
> @@ -29,6 +29,7 @@
>   * SUCH DAMAGE.
>   */
>  #include "box/lua/call.h"
> +#include "box/lua/misc.h"
>  #include "box/call.h"
>  #include "box/error.h"
>  #include "box/func.h"
> @@ -126,10 +127,31 @@ lbox_module_reload(lua_State *L)
>  	return 0;
>  }
>  
> +int
> +lbox_func_call(struct lua_State *L)
> +{
> +	if (lua_gettop(L) != 2 || !lua_isstring(L, 1))
> +		return luaL_error(L, "Usage func:call(table)");
> +
> +	size_t name_len;
> +	const char *name = lua_tolstring(L, 1, &name_len);
> +	size_t tuple_len;
> +	const char *tuple = lbox_encode_tuple_on_gc(L, 2, &tuple_len);

Please, create a port_lua from Lua stack and pass it to func_call.

> +
> +	struct port port;
> +	if (box_func_execute_by_name(name, name_len, &port, tuple,
> +				     tuple + tuple_len) != 0)

IMO we should return an error if the function isn't registered, not call
any existing Lua function. In other words, I think we should use
func_by_id/name + func_call here.

> +		return luaT_error(L);
> +	port_dump_lua(&port, L);
> +	port_destroy(&port);
> +	return 1;
> +}
> +
>  static const struct luaL_Reg boxlib_internal[] = {
>  	{"call_loadproc",  lbox_call_loadproc},
>  	{"sql_create_function",  lbox_sql_create_function},
>  	{"module_reload", lbox_module_reload},
> +	{"func_call", lbox_func_call},
>  	{NULL, NULL}
>  };
>  
> diff --git a/src/box/lua/init.c b/src/box/lua/init.c
> index 76b987b4b..67a822eb4 100644
> --- a/src/box/lua/init.c
> +++ b/src/box/lua/init.c
> @@ -39,6 +39,7 @@
>  
>  #include "box/box.h"
>  #include "box/txn.h"
> +#include "box/func.h"
>  #include "box/vclock.h"
>  
>  #include "box/lua/error.h"
> @@ -315,6 +316,7 @@ box_lua_init(struct lua_State *L)
>  	box_lua_xlog_init(L);
>  	box_lua_execute_init(L);
>  	luaopen_net_box(L);
> +	box_lua_func_init(L);
>  	lua_pop(L, 1);
>  	tarantool_lua_console_init(L);
>  	lua_pop(L, 1);
> diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
> index b5393e19a..1b09758d9 100644
> --- a/src/box/lua/schema.lua
> +++ b/src/box/lua/schema.lua
> @@ -2164,7 +2164,32 @@ function box.schema.func.exists(name_or_id)
>      return tuple ~= nil
>  end
>  
> +-- Helper function to check func:method() usage
> +local function check_func_arg(func, method)
> +    if type(func) ~= 'table' or func.name == nil then
> +        local fmt = 'Use func:%s(...) instead of func.%s(...)'
> +        error(string.format(fmt, method, method))

Please add a test case for this error.

> +    end
> +end
> +
> +local func_f_mt = {}
> +
> +func_f_mt.drop = function(func, opts)

func_f_mt? Why not simply func_mt?

> +    check_func_arg(func, 'drop')
> +    box.schema.func.drop(func.name, opts)
> +end
> +
> +func_f_mt.call = function(func, args)
> +    check_func_arg(func, 'call')
> +    return box.schema.func.call(func.name, args or {})
> +end
> +
> +function box.schema.func.bless(func)
> +    setmetatable(func, {__index = func_f_mt})
> +end
> +
>  box.schema.func.reload = internal.module_reload

Let's add 'reload' method to func_mt as well?

> +box.schema.func.call = internal.func_call
>  
>  box.internal.collation = {}
>  box.internal.collation.create = function(name, coll_type, locale, opts)
> diff --git a/src/box/schema.cc b/src/box/schema.cc
> index 0508482a6..90b18b347 100644
> --- a/src/box/schema.cc
> +++ b/src/box/schema.cc
> @@ -72,6 +72,7 @@ uint32_t space_cache_version = 0;
>  struct rlist on_schema_init = RLIST_HEAD_INITIALIZER(on_schema_init);
>  struct rlist on_alter_space = RLIST_HEAD_INITIALIZER(on_alter_space);
>  struct rlist on_alter_sequence = RLIST_HEAD_INITIALIZER(on_alter_sequence);
> +struct rlist on_alter_func = RLIST_HEAD_INITIALIZER(on_alter_func);
>  
>  /**
>   * Lock of scheme modification
> diff --git a/src/box/schema.h b/src/box/schema.h
> index 368463536..e569e3f9d 100644
> --- a/src/box/schema.h
> +++ b/src/box/schema.h
> @@ -130,6 +130,11 @@ int
>  schema_find_id(uint32_t system_space_id, uint32_t index_id, const char *name,
>  	       uint32_t len, uint32_t *object_id);
>  
> +struct func;
> +
> +struct func *
> +func_by_id(uint32_t fid);
> +
>  #if defined(__cplusplus)
>  } /* extern "C" */
>  
> @@ -178,11 +183,6 @@ func_cache_replace(struct func *new_func, struct func **old_func);
>  void
>  func_cache_delete(uint32_t fid);
>  
> -struct func;
> -
> -struct func *
> -func_by_id(uint32_t fid);
> -
>  static inline struct func *
>  func_cache_find(uint32_t fid)
>  {
> @@ -243,6 +243,12 @@ extern struct rlist on_alter_sequence;
>   */
>  extern struct rlist on_access_denied;
>  
> +/**
> + * Triggers fired after committing a change in _func space.
> + * It is passed the txn statement that altered the space.

txn statement? Really?

> + */
> +extern struct rlist on_alter_func;
> +
>  /**
>   * Context passed to on_access_denied trigger.
>   */
> diff --git a/test/box/persistent_func.result b/test/box/persistent_func.result
> index 54548f3b0..bf0a123e2 100644
> --- a/test/box/persistent_func.result
> +++ b/test/box/persistent_func.result
> @@ -86,6 +86,56 @@ math.abs(1)
>  box.schema.func.drop('monkey')
>  ---
>  ...
> +test_run:cmd("setopt delimiter ';'")
> +---
> +- true
> +...
> +body = [[function(array)
> +		local min = 999
> +		local max = -1
> +		for _, v in pairs(array) do
> +			min = math.min(min, v)
> +			max = math.max(max, v)
> +		end
> +		return min, max
> +	end]]
> +test_run:cmd("setopt delimiter ''");
> +---
> +...
> +box.schema.func.create("minmax", {body = body, opts = {is_sandboxed = true}})
> +---
> +...
> +box.func.minmax
> +---
> +- is_sandboxed: true
> +  returns: any
> +  is_deterministic: false
> +  id: 4
> +  language: LUA
> +  name: minmax
> +  is_persistent: true
> +  setuid: false
> +...
> +array = {1, 3, 2, 5, 9}
> +---
> +...
> +box.func.minmax:call({array})
> +---
> +- - 9
> +  - 1
> +...
> +conn:call("minmax", {array})
> +---
> +- 1
> +- 9
> +...
> +box.func.minmax:drop()
> +---
> +...
> +box.schema.func.exists("minmax")
> +---
> +- false
> +...
>  -- Test function with spell error - case 1.
>  test_run:cmd("setopt delimiter ';'")
>  ---
> @@ -170,3 +220,38 @@ box.schema.func.drop('test2')
>  box.schema.user.revoke('guest', 'execute', 'universe')
>  ---
>  ...
> +box.schema.func.create("secret", {body = "function() return 1 end"})
> +---
> +...
> +box.func.secret:call({})
> +---
> +- - 1
> +...
> +function secret_leak() return box.func.secret:call() end
> +---
> +...
> +box.schema.func.create('secret_leak')
> +---
> +...
> +box.schema.user.grant('guest', 'execute', 'function', 'secret_leak')
> +---
> +...
> +conn = net.connect(box.cfg.listen)
> +---
> +...
> +conn:call('secret_leak')
> +---
> +- error: Execute access to function 'secret' is denied for user 'guest'
> +...
> +conn:close()

Better use box.session.su for checking permissions.

> +---
> +...
> +box.schema.user.revoke('guest', 'execute', 'function', 'secret_leak')
> +---
> +...
> +box.schema.func.drop('secret_leak')
> +---
> +...
> +box.schema.func.drop('secret')
> +---
> +...



More information about the Tarantool-patches mailing list