From: Cyrill Gorcunov via Tarantool-patches <tarantool-patches@dev.tarantool.org> To: tml <tarantool-patches@dev.tarantool.org> Cc: Vladislav Shpilevoy <v.shpilevoy@tarantool.org> Subject: [Tarantool-patches] [PATCH v19 4/6] box/func: switch to module_cache interface Date: Tue, 2 Mar 2021 00:23:41 +0300 [thread overview] Message-ID: <20210301212343.422372-5-gorcunov@gmail.com> (raw) In-Reply-To: <20210301212343.422372-1-gorcunov@gmail.com> This allows us to drop some low-level functions such as package lookup, module loading and symbol resolving. The "box.schema.func" engine operates as a second level cache over module_cache engine. This is because of module reloading procedure: "box.schema.func" allows explicit module reload only, thus even if a module has been changed on disk it should not be invalidated in second level cache. And we can't change this due to backward compatibility rule. Thus: - we introduce box_module structure which carries low-level module instance, and update lowlevel cache in passthrough way; - func_c structure (which represent C function) now uses low-level module_func structure; - cache routines now works with box_module itself; - function manipulations are moved to func_c_create, func_c_load func_c_unload helpers; - all variables with type func_c now named as func_c, plain func name is kept for general functions, otherwise it is hard to figure out which exactly instance you work with, just to be consistent in names because massive code rework is done anyway; - due to lazy resolving of function symbols keep func_c_load name for exactly function loading and symbol resolving is done in func_c_resolve helper; - make func_c_call static as it should be. Part-of #4642 Signed-off-by: Cyrill Gorcunov <gorcunov@gmail.com> --- src/box/func.c | 492 ++++++++++++++++++--------------------------- src/box/func.h | 19 +- src/box/func_def.h | 14 -- 3 files changed, 200 insertions(+), 325 deletions(-) diff --git a/src/box/func.c b/src/box/func.c index 88903f40e..a8401c6ed 100644 --- a/src/box/func.c +++ b/src/box/func.c @@ -30,19 +30,12 @@ */ #include "func.h" #include "fiber.h" -#include "trivia/config.h" #include "assoc.h" -#include "lua/utils.h" #include "lua/call.h" -#include "error.h" #include "errinj.h" #include "diag.h" -#include "port.h" #include "schema.h" #include "session.h" -#include "libeio/eio.h" -#include <fcntl.h> -#include <dlfcn.h> /** * Parsed symbol and package names. @@ -56,6 +49,18 @@ struct func_name { const char *package_end; }; +/** + * Dynamic shared module. + */ +struct box_module { + /** Low level module instance. */ + struct module *module; + /** List of imported functions. */ + struct rlist funcs; + /** Number of active references. */ + int64_t refs; +}; + struct func_c { /** Function object base class. */ struct func base; @@ -64,16 +69,18 @@ struct func_c { */ struct rlist item; /** - * For C functions, the body of the function. + * Back reference to a cached module. */ - box_function_f func; + struct box_module *bxmod; /** - * Each stored function keeps a handle to the - * dynamic library for the C callback. + * C function to call. */ - struct box_module *bxmod; + struct module_func mf; }; +/** Hash from module name to its box_module instance. */ +static struct mh_strnptr_t *box_module_hash = NULL; + /*** * Split function name to symbol and package names. * For example, str = foo.bar.baz => sym = baz, package = foo.bar @@ -95,82 +102,7 @@ func_split_name(const char *str, struct func_name *name) } } -/** - * Arguments for luaT_module_find used by lua_cpcall() - */ -struct module_find_ctx { - const char *package; - const char *package_end; - char *path; - size_t path_len; -}; - -/** - * A cpcall() helper for module_find() - */ -static int -luaT_module_find(lua_State *L) -{ - struct module_find_ctx *ctx = (struct module_find_ctx *) - lua_topointer(L, 1); - - /* - * Call package.searchpath(name, package.cpath) and use - * the path to the function in dlopen(). - */ - lua_getglobal(L, "package"); - - lua_getfield(L, -1, "search"); - - /* Argument of search: name */ - lua_pushlstring(L, ctx->package, ctx->package_end - ctx->package); - - lua_call(L, 1, 1); - if (lua_isnil(L, -1)) - return luaL_error(L, "module not found"); - /* Convert path to absolute */ - char resolved[PATH_MAX]; - if (realpath(lua_tostring(L, -1), resolved) == NULL) { - diag_set(SystemError, "realpath"); - return luaT_error(L); - } - - snprintf(ctx->path, ctx->path_len, "%s", resolved); - return 0; -} - -/** - * Find path to module using Lua's package.cpath - * @param package package name - * @param package_end a pointer to the last byte in @a package + 1 - * @param[out] path path to shared library - * @param path_len size of @a path buffer - * @retval 0 on success - * @retval -1 on error, diag is set - */ -static int -module_find(const char *package, const char *package_end, char *path, - size_t path_len) -{ - struct module_find_ctx ctx = { package, package_end, path, path_len }; - lua_State *L = tarantool_L; - int top = lua_gettop(L); - if (luaT_cpcall(L, luaT_module_find, &ctx) != 0) { - int package_len = (int) (package_end - package); - diag_set(ClientError, ER_LOAD_MODULE, package_len, package, - lua_tostring(L, -1)); - lua_settop(L, top); - return -1; - } - assert(top == lua_gettop(L)); /* cpcall discard results */ - return 0; -} - -static struct mh_strnptr_t *box_module_hash = NULL; - -static void -box_module_gc(struct box_module *bxmod); - +/** Initialize box module subsystem. */ int box_module_init(void) { @@ -183,20 +115,6 @@ box_module_init(void) return 0; } -void -box_module_free(void) -{ - while (mh_size(box_module_hash) > 0) { - struct box_module *bxmod; - - mh_int_t i = mh_first(box_module_hash); - bxmod = mh_strnptr_node(box_module_hash, i)->val; - /* Can't delete modules if they have active calls */ - box_module_gc(bxmod); - } - mh_strnptr_delete(box_module_hash); -} - /** * Look up a module in the modules cache. */ @@ -216,10 +134,15 @@ cache_find(const char *name, const char *name_end) static int cache_put(struct box_module *bxmod) { - size_t package_len = strlen(bxmod->package); - uint32_t name_hash = mh_strn_hash(bxmod->package, package_len); + const char *package = bxmod->module->package; + size_t package_len = bxmod->module->package_len; + const struct mh_strnptr_node_t strnode = { - bxmod->package, package_len, name_hash, bxmod}; + .str = package, + .len = package_len, + .hash = mh_strn_hash(package, package_len), + .val = bxmod + }; mh_int_t i = mh_strnptr_put(box_module_hash, &strnode, NULL, NULL); if (i == mh_end(box_module_hash)) { @@ -231,150 +154,175 @@ cache_put(struct box_module *bxmod) } /** - * Delete a module from the module cache + * Update module in module cache. */ static void -cache_del(const char *name, const char *name_end) +cache_update(struct box_module *bxmod) { - mh_int_t i = mh_strnptr_find_inp(box_module_hash, name, - name_end - name); - if (i == mh_end(box_module_hash)) - return; - mh_strnptr_del(box_module_hash, i, NULL); + const char *str = bxmod->module->package; + size_t len = bxmod->module->package_len; + + mh_int_t e = mh_strnptr_find_inp(box_module_hash, str, len); + if (e == mh_end(box_module_hash)) + panic("func: failed to update cache: %s", str); + + mh_strnptr_node(box_module_hash, e)->str = str; + mh_strnptr_node(box_module_hash, e)->val = bxmod; } -/* - * Load a dso. - * Create a new symlink based on temporary directory and try to - * load via this symink to load a dso twice for cases of a function - * reload. +/** + * Delete a module from the module cache */ +static void +cache_del(struct box_module *bxmod) +{ + const char *str = bxmod->module->package; + size_t len = bxmod->module->package_len; + + mh_int_t e = mh_strnptr_find_inp(box_module_hash, str, len); + if (e != mh_end(box_module_hash)) { + struct box_module *v; + v = mh_strnptr_node(box_module_hash, e)->val; + if (v == bxmod) + mh_strnptr_del(box_module_hash, e, NULL); + } +} + +/** Increment reference to a module. */ +static inline void +box_module_ref(struct box_module *bxmod) +{ + assert(bxmod->refs >= 0); + ++bxmod->refs; +} + +/** Low-level module loader. */ static struct box_module * -box_module_load(const char *package, const char *package_end) +box_module_ld(const char *package, size_t package_len, + struct module *(ld)(const char *package, + size_t package_len)) { - char path[PATH_MAX]; - if (module_find(package, package_end, path, sizeof(path)) != 0) + struct module *m = ld(package, package_len); + if (m == NULL) return NULL; - int package_len = package_end - package; - struct box_module *bxmod = malloc(sizeof(*bxmod) + package_len + 1); + struct box_module *bxmod = malloc(sizeof(*bxmod)); if (bxmod == NULL) { + module_unload(m); diag_set(OutOfMemory, sizeof(*bxmod) + package_len + 1, "malloc", "struct box_module"); return NULL; } - memcpy(bxmod->package, package, package_len); - bxmod->package[package_len] = 0; - rlist_create(&bxmod->funcs); - bxmod->calls = 0; - - const char *tmpdir = getenv("TMPDIR"); - if (tmpdir == NULL) - tmpdir = "/tmp"; - char dir_name[PATH_MAX]; - int rc = snprintf(dir_name, sizeof(dir_name), "%s/tntXXXXXX", tmpdir); - if (rc < 0 || (size_t) rc >= sizeof(dir_name)) { - diag_set(SystemError, "failed to generate path to tmp dir"); - goto error; - } - - if (mkdtemp(dir_name) == NULL) { - diag_set(SystemError, "failed to create unique dir name: %s", - dir_name); - goto error; - } - char load_name[PATH_MAX]; - rc = snprintf(load_name, sizeof(load_name), "%s/%.*s." TARANTOOL_LIBEXT, - dir_name, package_len, package); - if (rc < 0 || (size_t) rc >= sizeof(dir_name)) { - diag_set(SystemError, "failed to generate path to DSO"); - goto error; - } - struct stat st; - if (stat(path, &st) < 0) { - diag_set(SystemError, "failed to stat() module %s", path); - goto error; - } + bxmod->refs = 0; + bxmod->module = m; + rlist_create(&bxmod->funcs); - int source_fd = open(path, O_RDONLY); - if (source_fd < 0) { - diag_set(SystemError, "failed to open module %s file for" \ - " reading", path); - goto error; - } - int dest_fd = open(load_name, O_WRONLY|O_CREAT|O_TRUNC, - st.st_mode & 0777); - if (dest_fd < 0) { - diag_set(SystemError, "failed to open file %s for writing ", - load_name); - close(source_fd); - goto error; - } + box_module_ref(bxmod); + return bxmod; +} - off_t ret = eio_sendfile_sync(dest_fd, source_fd, 0, st.st_size); - close(source_fd); - close(dest_fd); - if (ret != st.st_size) { - diag_set(SystemError, "failed to copy DSO %s to %s", - path, load_name); - goto error; - } +/** Load a new module. */ +static struct box_module * +box_module_load(const char *package, size_t package_len) +{ + return box_module_ld(package, package_len, + module_load); +} - bxmod->handle = dlopen(load_name, RTLD_NOW | RTLD_LOCAL); - if (unlink(load_name) != 0) - say_warn("failed to unlink dso link %s", load_name); - if (rmdir(dir_name) != 0) - say_warn("failed to delete temporary dir %s", dir_name); - if (bxmod->handle == NULL) { - diag_set(ClientError, ER_LOAD_MODULE, package_len, - package, dlerror()); - goto error; - } - struct errinj *e = errinj(ERRINJ_DYN_MODULE_COUNT, ERRINJ_INT); - if (e != NULL) - ++e->iparam; - return bxmod; -error: - free(bxmod); - return NULL; +/** Load a new module with force cache invalidation. */ +static struct box_module * +box_module_load_force(const char *package, size_t package_len) +{ + return box_module_ld(package, package_len, + module_load_force); } +/** Delete a module. */ static void box_module_delete(struct box_module *bxmod) { struct errinj *e = errinj(ERRINJ_DYN_MODULE_COUNT, ERRINJ_INT); if (e != NULL) --e->iparam; - dlclose(bxmod->handle); + module_unload(bxmod->module); TRASH(bxmod); free(bxmod); } -/* - * Check if a dso is unused and can be closed. - */ -static void -box_module_gc(struct box_module *bxmod) +/** Decrement reference to a module and delete it if last one. */ +static inline void +box_module_unref(struct box_module *bxmod) { - if (rlist_empty(&bxmod->funcs) && bxmod->calls == 0) + assert(bxmod->refs > 0); + if (--bxmod->refs == 0) { + cache_del(bxmod); box_module_delete(bxmod); + } } -/* - * Import a function from the module. - */ -static box_function_f -box_module_sym(struct box_module *bxmod, const char *name) +/** Free box modules subsystem. */ +void +box_module_free(void) { - box_function_f f = (box_function_f)dlsym(bxmod->handle, name); - if (f == NULL) { - diag_set(ClientError, ER_LOAD_FUNCTION, name, dlerror()); - return NULL; + while (mh_size(box_module_hash) > 0) { + struct box_module *bxmod; + + mh_int_t i = mh_first(box_module_hash); + bxmod = mh_strnptr_node(box_module_hash, i)->val; + /* + * Module won't be deleted if it has + * active functions bound. + */ + box_module_unref(bxmod); } - return f; + mh_strnptr_delete(box_module_hash); +} + +static struct func_vtab func_c_vtab; + +/** Create new box function. */ +static inline void +func_c_create(struct func_c *func_c) +{ + func_c->bxmod = NULL; + func_c->base.vtab = &func_c_vtab; + rlist_create(&func_c->item); + module_func_create(&func_c->mf); +} + +/** Test if function is not resolved. */ +static inline bool +is_func_c_emtpy(struct func_c *func_c) +{ + return is_module_func_empty(&func_c->mf); } +/** Load a new function. */ +static inline int +func_c_load(struct box_module *bxmod, const char *func_name, + struct func_c *func_c) +{ + int rc = module_func_load(bxmod->module, func_name, &func_c->mf); + if (rc == 0) { + rlist_move(&bxmod->funcs, &func_c->item); + func_c->bxmod = bxmod; + box_module_ref(bxmod); + } + return rc; +} + +/** Unload a function. */ +static inline void +func_c_unload(struct func_c *func_c) +{ + module_func_unload(&func_c->mf); + rlist_del(&func_c->item); + box_module_unref(func_c->bxmod); + func_c_create(func_c); +} + +/** Reload module in a force way. */ int box_module_reload(const char *package, const char *package_end) { @@ -385,25 +333,24 @@ box_module_reload(const char *package, const char *package_end) return -1; } - struct box_module *bxmod = box_module_load(package, package_end); + size_t len = package_end - package; + struct box_module *bxmod = box_module_load_force(package, len); if (bxmod == NULL) return -1; - struct func_c *func, *tmp_func; - rlist_foreach_entry_safe(func, &bxmod_old->funcs, item, tmp_func) { + struct func_c *func, *tmp; + rlist_foreach_entry_safe(func, &bxmod_old->funcs, item, tmp) { struct func_name name; func_split_name(func->base.def->name, &name); - func->func = box_module_sym(bxmod, name.sym); - if (func->func == NULL) + func_c_unload(func); + if (func_c_load(bxmod, name.sym, func) != 0) goto restore; - func->bxmod = bxmod; - rlist_move(&bxmod->funcs, &func->item); } - cache_del(package, package_end); - if (cache_put(bxmod) != 0) - goto restore; - box_module_gc(bxmod_old); + + cache_update(bxmod); + box_module_unref(bxmod_old); return 0; + restore: /* * Some old-dso func can't be load from new module, restore old @@ -412,8 +359,8 @@ box_module_reload(const char *package, const char *package_end) do { struct func_name name; func_split_name(func->base.def->name, &name); - func->func = box_module_sym(bxmod_old, name.sym); - if (func->func == NULL) { + + if (func_c_load(bxmod_old, name.sym, func) != 0) { /* * Something strange was happen, an early loaden * function was not found in an old dso. @@ -421,12 +368,10 @@ box_module_reload(const char *package, const char *package_end) panic("Can't restore module function, " "server state is inconsistent"); } - func->bxmod = bxmod_old; - rlist_move(&bxmod_old->funcs, &func->item); } while (func != rlist_first_entry(&bxmod_old->funcs, struct func_c, item)); assert(rlist_empty(&bxmod->funcs)); - box_module_delete(bxmod); + box_module_unref(bxmod); return -1; } @@ -437,6 +382,7 @@ func_c_new(struct func_def *def); extern struct func * func_sql_builtin_new(struct func_def *def); +/** Allocate a new function. */ struct func * func_new(struct func_def *def) { @@ -474,67 +420,51 @@ func_new(struct func_def *def) return func; } -static struct func_vtab func_c_vtab; - +/** Create new C function. */ static struct func * func_c_new(MAYBE_UNUSED struct func_def *def) { assert(def->language == FUNC_LANGUAGE_C); assert(def->body == NULL && !def->is_sandboxed); - struct func_c *func = (struct func_c *) malloc(sizeof(struct func_c)); - if (func == NULL) { - diag_set(OutOfMemory, sizeof(*func), "malloc", "func"); + struct func_c *func_c = malloc(sizeof(struct func_c)); + if (func_c == NULL) { + diag_set(OutOfMemory, sizeof(*func_c), "malloc", "func_c"); return NULL; } - func->base.vtab = &func_c_vtab; - func->func = NULL; - func->bxmod = NULL; - return &func->base; -} - -static void -func_c_unload(struct func_c *func) -{ - if (func->bxmod) { - rlist_del(&func->item); - if (rlist_empty(&func->bxmod->funcs)) { - struct func_name name; - func_split_name(func->base.def->name, &name); - cache_del(name.package, name.package_end); - } - box_module_gc(func->bxmod); - } - func->bxmod = NULL; - func->func = NULL; + func_c_create(func_c); + return &func_c->base; } +/** Destroy C function. */ static void func_c_destroy(struct func *base) { assert(base->vtab == &func_c_vtab); assert(base != NULL && base->def->language == FUNC_LANGUAGE_C); - struct func_c *func = (struct func_c *) base; - func_c_unload(func); + struct func_c *func_c = (struct func_c *) base; + box_module_unref(func_c->bxmod); + func_c_unload(func_c); TRASH(base); - free(func); + free(func_c); } /** - * Resolve func->func (find the respective DLL and fetch the + * Resolve func->func (find the respective share library and fetch the * symbol from it). */ static int -func_c_load(struct func_c *func) +func_c_resolve(struct func_c *func_c) { - assert(func->func == NULL); + assert(is_func_c_emtpy(func_c)); struct func_name name; - func_split_name(func->base.def->name, &name); + func_split_name(func_c->base.def->name, &name); struct box_module *cached, *bxmod; cached = cache_find(name.package, name.package_end); if (cached == NULL) { - bxmod = box_module_load(name.package, name.package_end); + size_t len = name.package_end - name.package; + bxmod = box_module_load(name.package, len); if (bxmod == NULL) return -1; if (cache_put(bxmod) != 0) { @@ -545,8 +475,7 @@ func_c_load(struct func_c *func) bxmod = cached; } - func->func = box_module_sym(bxmod, name.sym); - if (func->func == NULL) { + if (func_c_load(bxmod, name.sym, func_c) != 0) { if (cached == NULL) { /* * In case if it was a first load we should @@ -557,54 +486,27 @@ func_c_load(struct func_c *func) * Note the box_module_sym set an error thus be * careful to not wipe it. */ - cache_del(name.package, name.package_end); + cache_del(bxmod); box_module_delete(bxmod); } return -1; } - func->bxmod = bxmod; - rlist_add(&bxmod->funcs, &func->item); return 0; } -int +/** Execute C function. */ +static int func_c_call(struct func *base, struct port *args, struct port *ret) { assert(base->vtab == &func_c_vtab); assert(base != NULL && base->def->language == FUNC_LANGUAGE_C); - struct func_c *func = (struct func_c *) base; - if (func->func == NULL) { - if (func_c_load(func) != 0) + struct func_c *func_c = (struct func_c *) base; + if (is_func_c_emtpy(func_c)) { + if (func_c_resolve(func_c) != 0) return -1; } - struct region *region = &fiber()->gc; - size_t region_svp = region_used(region); - uint32_t data_sz; - const char *data = port_get_msgpack(args, &data_sz); - if (data == NULL) - return -1; - - port_c_create(ret); - box_function_ctx_t ctx = { ret }; - - /* Module can be changed after function reload. */ - struct box_module *bxmod = func->bxmod; - assert(bxmod != NULL); - ++bxmod->calls; - int rc = func->func(&ctx, data, data + data_sz); - --bxmod->calls; - box_module_gc(bxmod); - region_truncate(region, region_svp); - if (rc != 0) { - if (diag_last_error(&fiber()->diag) == NULL) { - /* Stored procedure forget to set diag */ - diag_set(ClientError, ER_PROC_C, "unknown error"); - } - port_destroy(ret); - return -1; - } - return rc; + return module_func_call(&func_c->mf, args, ret); } static struct func_vtab func_c_vtab = { diff --git a/src/box/func.h b/src/box/func.h index 66e4d09bc..2be33a812 100644 --- a/src/box/func.h +++ b/src/box/func.h @@ -37,6 +37,7 @@ #include "small/rlist.h" #include "func_def.h" #include "user_def.h" +#include "module_cache.h" #if defined(__cplusplus) extern "C" { @@ -44,20 +45,6 @@ extern "C" { struct func; -/** - * Dynamic shared module. - */ -struct box_module { - /** Module dlhandle. */ - void *handle; - /** List of imported functions. */ - struct rlist funcs; - /** Count of active calls. */ - size_t calls; - /** Module's package name. */ - char package[0]; -}; - /** Virtual method table for func object. */ struct func_vtab { /** Call function with given arguments. */ @@ -85,13 +72,13 @@ struct func { }; /** - * Initialize modules subsystem. + * Initialize box modules subsystem. */ int box_module_init(void); /** - * Cleanup modules subsystem. + * Cleanup box modules subsystem. */ void box_module_free(void); diff --git a/src/box/func_def.h b/src/box/func_def.h index d99d89190..75cd6a0d3 100644 --- a/src/box/func_def.h +++ b/src/box/func_def.h @@ -168,20 +168,6 @@ func_def_dup(struct func_def *def); int func_def_check(struct func_def *def); -/** - * API of C stored function. - */ - -struct port; - -struct box_function_ctx { - struct port *port; -}; - -typedef struct box_function_ctx box_function_ctx_t; -typedef int (*box_function_f)(box_function_ctx_t *ctx, - const char *args, const char *args_end); - #ifdef __cplusplus } #endif -- 2.29.2
next prev parent reply other threads:[~2021-03-01 21:26 UTC|newest] Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top 2021-03-01 21:23 [Tarantool-patches] [PATCH v19 0/6] box: implement cmod Lua module Cyrill Gorcunov via Tarantool-patches 2021-03-01 21:23 ` [Tarantool-patches] [PATCH v19 1/6] box/func: module_reload -- drop redundant argument Cyrill Gorcunov via Tarantool-patches 2021-03-01 21:23 ` [Tarantool-patches] [PATCH v19 2/6] box/func: prepare for transition to modules subsystem Cyrill Gorcunov via Tarantool-patches 2021-03-08 22:44 ` Vladislav Shpilevoy via Tarantool-patches 2021-03-01 21:23 ` [Tarantool-patches] [PATCH v19 3/6] box/module_cache: introduce " Cyrill Gorcunov via Tarantool-patches 2021-03-08 22:45 ` Vladislav Shpilevoy via Tarantool-patches 2021-03-01 21:23 ` Cyrill Gorcunov via Tarantool-patches [this message] 2021-03-08 22:47 ` [Tarantool-patches] [PATCH v19 4/6] box/func: switch to module_cache interface Vladislav Shpilevoy via Tarantool-patches 2021-03-01 21:23 ` [Tarantool-patches] [PATCH v19 5/6] box/cmod: implement cmod Lua module Cyrill Gorcunov via Tarantool-patches 2021-03-08 22:51 ` Vladislav Shpilevoy via Tarantool-patches 2021-03-01 21:23 ` [Tarantool-patches] [PATCH v19 6/6] test: box/cfunc -- add cmod test Cyrill Gorcunov via Tarantool-patches 2021-03-08 22:51 ` Vladislav Shpilevoy via Tarantool-patches
Reply instructions: You may reply publicly to this message via plain-text email using any one of the following methods: * Save the following mbox file, import it into your mail client, and reply-to-all from there: mbox Avoid top-posting and favor interleaved quoting: https://en.wikipedia.org/wiki/Posting_style#Interleaved_style * Reply using the --to, --cc, and --in-reply-to switches of git-send-email(1): git send-email \ --in-reply-to=20210301212343.422372-5-gorcunov@gmail.com \ --to=tarantool-patches@dev.tarantool.org \ --cc=gorcunov@gmail.com \ --cc=v.shpilevoy@tarantool.org \ --subject='Re: [Tarantool-patches] [PATCH v19 4/6] box/func: switch to module_cache interface' \ /path/to/YOUR_REPLY https://kernel.org/pub/software/scm/git/docs/git-send-email.html * If your mail client supports setting the In-Reply-To header via mailto: links, try the mailto: link
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox