diff --git a/changelogs/unreleased/implement-on-shutdown-api.md b/changelogs/unreleased/implement-on-shutdown-api.md new file mode 100644 index 0000000000000000000000000000000000000000..c7d7425796595db80f9c8e96fe514404a28c73f9 --- /dev/null +++ b/changelogs/unreleased/implement-on-shutdown-api.md @@ -0,0 +1,5 @@ +## feature/core + +* Implemented on_shutdown API, which allows tarantool module + developer to register functions that will be called when + tarantool stopped (gh-5723). \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 76c410a7010bf55953cd1bd5546ed6b560d3801f..fb528b666644a9e0bad34f845c88bf76ca20e407 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -98,6 +98,8 @@ add_library(crc32 STATIC ) target_link_libraries(crc32 cpu_feature) +add_library(shutdown STATIC on_shutdown.c) + set (server_sources find_path.c curl.c @@ -142,6 +144,7 @@ set (server_sources set(api_headers ${CMAKE_BINARY_DIR}/src/trivia/config.h ${CMAKE_SOURCE_DIR}/src/trivia/util.h + ${CMAKE_SOURCE_DIR}/src/on_shutdown.h ${CMAKE_SOURCE_DIR}/src/lib/core/say.h ${CMAKE_SOURCE_DIR}/src/lib/core/fiber.h ${CMAKE_SOURCE_DIR}/src/lib/core/fiber_cond.h @@ -187,7 +190,7 @@ target_link_libraries(server core coll http_parser bit uri uuid swim swim_udp # Rule of thumb: if exporting a symbol from a static library, list the # library here. set (reexport_libraries server core misc bitset csv swim swim_udp swim_ev - ${LUAJIT_LIBRARIES} ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES}) + shutdown ${LUAJIT_LIBRARIES} ${MSGPUCK_LIBRARIES} ${ICU_LIBRARIES}) set (common_libraries ${reexport_libraries} diff --git a/src/exports.h b/src/exports.h index 1b95e75f627b25990a2e04081cbf50e7d8ec1255..eb72bc928e56fb70a58c00a5aa45046f1d818f4c 100644 --- a/src/exports.h +++ b/src/exports.h @@ -48,6 +48,7 @@ EXPORT(box_latch_lock) EXPORT(box_latch_new) EXPORT(box_latch_trylock) EXPORT(box_latch_unlock) +EXPORT(box_on_shutdown) EXPORT(box_region_aligned_alloc) EXPORT(box_region_alloc) EXPORT(box_region_truncate) diff --git a/src/on_shutdown.c b/src/on_shutdown.c new file mode 100644 index 0000000000000000000000000000000000000000..5444f802e38d3f775b43d47c864bf5c56115b985 --- /dev/null +++ b/src/on_shutdown.c @@ -0,0 +1,130 @@ +/* + * Copyright 2010-2021, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY AUTHORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "on_shutdown.h" + +#include "box/box.h" +#include "say.h" + +#include <stdlib.h> +#include <small/rlist.h> +#include <errno.h> +#include <trigger.h> + +struct on_shutdown_trigger { + struct trigger trigger; + /** Shutdown trigger function */ + int (*handler)(void *); + /** Trigger function argument */ + void *arg; + /** link for on_shutdown_trigger_list */ + struct rlist link; +}; + +static struct rlist on_shutdown_trigger_list = + RLIST_HEAD_INITIALIZER(on_shutdown_trigger_list); + +static int +trigger_commom_f(struct trigger *trigger, MAYBE_UNUSED void *event) +{ + struct on_shutdown_trigger *on_shutdown_trigger = + container_of(trigger, struct on_shutdown_trigger, trigger); + return on_shutdown_trigger->handler(on_shutdown_trigger->arg); +} + +static int +on_shutdown_trigger_create(int (*handler)(void *), void *arg) +{ + struct on_shutdown_trigger *trigger = (struct on_shutdown_trigger *) + malloc(sizeof(struct on_shutdown_trigger)); + if (trigger == NULL) + return -1; + trigger_create(&trigger->trigger, trigger_commom_f, NULL, NULL); + trigger->handler = handler; + trigger->arg = arg; + trigger_add(&box_on_shutdown_trigger_list, &trigger->trigger); + rlist_add_entry(&on_shutdown_trigger_list, trigger, link); + return 0; +} + +API_EXPORT int +box_on_shutdown(void *arg, int (*new_handler)(void *), + int (*old_handler)(void *)) +{ + struct on_shutdown_trigger *trigger; + if (old_handler == NULL) { + if (new_handler == NULL) { + /* + * Invalid function params, old_handler or new_handler + * must be set + */ + say_error("Invalid function argument: old_handler and " + "new_handler cannot be equal to zero at the " + "same time."); + errno = EINVAL; + return -1; + } + return on_shutdown_trigger_create(new_handler, arg); + } + + rlist_foreach_entry(trigger, &on_shutdown_trigger_list, link) { + if (trigger->handler == old_handler) { + if (new_handler != NULL) { + /* + * Change on_shutdown trigger handler, and arg + */ + trigger->handler = new_handler; + trigger->arg = arg; + } else { + /* + * In case new_handler == NULL + * Remove old on_shutdown trigger and + * destroy it + */ + trigger_clear(&trigger->trigger); + rlist_del_entry(trigger, link); + free(trigger); + } + return 0; + } + } + + /* + * Here we are in case when we not find on_shutdown trigger, + * which we want to destroy return -1. + */ + say_error("Invalid function argument: previously registered trigger " + "with handler == old_handler not found."); + errno = EINVAL; + return -1; +} + + diff --git a/src/on_shutdown.h b/src/on_shutdown.h new file mode 100644 index 0000000000000000000000000000000000000000..3afb5767cda48cdc69c201965826d4ba3b1af285 --- /dev/null +++ b/src/on_shutdown.h @@ -0,0 +1,66 @@ +#pragma once +/* + * Copyright 2010-2021, Tarantool AUTHORS, please see AUTHORS file. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +#include <trivia/util.h> + +#if defined(__cplusplus) +extern "C" { +#endif /* defined(__cplusplus) */ + +/** \cond public */ + +/** + * Function, which registers or deletes on_shutdown handler. + * @param[in] arg on_shutdown function's argument. + * @param[in] new_handler New on_shutdown handler, in + * case this argument is NULL, function finds + * and destroys old on_shutdown handler. + * @param[in] old_handler Old on_shutdown handler. + * @retval return 0 if success othrewise return -1 and sets + * errno. There are three cases when + * function fails: + * - both old_handler and new_handler are equal to + * zero (sets errno to EINVAL). + * - old_handler != NULL, but there is no trigger + * with such function (sets errno to EINVAL). + * - malloc for some internal struct memory allocation + * return NULL (errno sets by malloc to ENOMEM). + */ +API_EXPORT int +box_on_shutdown(void *arg, int (*new_hadler)(void *), + int (*old_handler)(void *)); + +/** \endcond public */ + +#if defined(__cplusplus) +} /* extern "C" */ +#endif /* defined(__cplusplus) */ + diff --git a/test/app/CMakeLists.txt b/test/app/CMakeLists.txt index 059ee8f3d63b9dd401339760d592efe22a537bcd..7e509bc4c420847933491a388fddd96684e138d7 100644 --- a/test/app/CMakeLists.txt +++ b/test/app/CMakeLists.txt @@ -1 +1,2 @@ build_module(loaderslib loaderslib.c) +build_module(on_shutdownlib on_shutdownlib.c) diff --git a/test/app/on_shutdown.lua b/test/app/on_shutdown.lua new file mode 100644 index 0000000000000000000000000000000000000000..72063552c3d46a4491700ed92273b609102c7375 --- /dev/null +++ b/test/app/on_shutdown.lua @@ -0,0 +1,7 @@ +#!/usr/bin/env tarantool + +require('console').listen(os.getenv('ADMIN')) + +box.cfg({ + listen = os.getenv("LISTEN"), +}) diff --git a/test/app/on_shutdown.result b/test/app/on_shutdown.result new file mode 100644 index 0000000000000000000000000000000000000000..c3f86cdeda9e1a61826d1d4b1553ed1f53c160e1 --- /dev/null +++ b/test/app/on_shutdown.result @@ -0,0 +1,136 @@ +-- test-run result file version 2 +env = require('test_run') + | --- + | ... +test_run = env.new() + | --- + | ... +build_dir = os.getenv("BUILDDIR") .. "/test/app/" + | --- + | ... +soext = (jit.os == "OSX" and "dylib" or "so") + | --- + | ... +on_shutdownlib = "on_shutdownlib."..soext + | --- + | ... +os.execute(string.format("cp %s/%s .", build_dir, on_shutdownlib)) + | --- + | - 0 + | ... + +test_run:cmd('create server test with script="app/on_shutdown.lua"') + | --- + | - true + | ... +test_run:cmd('start server test') + | --- + | - true + | ... +test_run:cmd('switch test') + | --- + | - true + | ... + +-- Default on_shutdown triggers timeout == 3, but we sets it +-- here directly to make test clear +box.ctl.set_on_shutdown_timeout(3) + | --- + | ... +module = require('on_shutdownlib') + | --- + | ... +-- Setting shutdown timeout for main module fiber function. +-- This timeout < on_shutdown triggers timeout, so on_shutdown +-- trigger completes successfully. +shutdown_timeout = 1 + | --- + | ... +module.cfg(shutdown_timeout) + | --- + | ... + +test_run:cmd('switch default') + | --- + | - true + | ... +test_run:cmd('stop server test') + | --- + | - true + | ... +os.execute(string.format("grep -r \"stop module fiber\" on_shutdown.log")) + | --- + | - 0 + | ... +os.execute(string.format("grep -r \"join module fiber\" on_shutdown.log")) + | --- + | - 0 + | ... +os.execute(string.format("grep -r \"module_fiber_f finished\" \ + on_shutdown.log")) + | --- + | - 0 + | ... +os.execute(string.format("grep -r \"join module fiber finished\" \ + on_shutdown.log")) + | --- + | - 0 + | ... +test_run:cmd('start server test') + | --- + | - true + | ... +test_run:cmd('switch test') + | --- + | - true + | ... + +box.ctl.set_on_shutdown_timeout(1) + | --- + | ... +module = require('on_shutdownlib') + | --- + | ... +-- Setting shutdown timeout for main module fiber function. +-- This timeout > on_shutdown triggers timeout, so on_shutdown +-- trigger does not have time to complete. +shutdown_timeout = 10 + | --- + | ... +module.cfg(shutdown_timeout) + | --- + | ... + +test_run:cmd('switch default') + | --- + | - true + | ... +test_run:cmd('stop server test') + | --- + | - true + | ... +os.execute(string.format("rm %s", on_shutdownlib)) + | --- + | - 0 + | ... +os.execute(string.format("grep -r \"stop module fiber\" on_shutdown.log")) + | --- + | - 0 + | ... +os.execute(string.format("grep -r \"join module fiber\" on_shutdown.log")) + | --- + | - 0 + | ... +os.execute(string.format("grep -r \"SystemError timed out\" on_shutdown.log")) + | --- + | - 0 + | ... + +test_run:cmd('cleanup server test') + | --- + | - true + | ... +test_run:cmd('delete server test') + | --- + | - true + | ... diff --git a/test/app/on_shutdown.test.lua b/test/app/on_shutdown.test.lua new file mode 100644 index 0000000000000000000000000000000000000000..c133209b2d661d7d9b9aa21a02b6939e9533c07a --- /dev/null +++ b/test/app/on_shutdown.test.lua @@ -0,0 +1,49 @@ +env = require('test_run') +test_run = env.new() +build_dir = os.getenv("BUILDDIR") .. "/test/app/" +soext = (jit.os == "OSX" and "dylib" or "so") +on_shutdownlib = "on_shutdownlib."..soext +os.execute(string.format("cp %s/%s .", build_dir, on_shutdownlib)) + +test_run:cmd('create server test with script="app/on_shutdown.lua"') +test_run:cmd('start server test') +test_run:cmd('switch test') + +-- Default on_shutdown triggers timeout == 3, but we sets it +-- here directly to make test clear +box.ctl.set_on_shutdown_timeout(3) +module = require('on_shutdownlib') +-- Setting shutdown timeout for main module fiber function. +-- This timeout < on_shutdown triggers timeout, so on_shutdown +-- trigger completes successfully. +shutdown_timeout = 1 +module.cfg(shutdown_timeout) + +test_run:cmd('switch default') +test_run:cmd('stop server test') +os.execute(string.format("grep -r \"stop module fiber\" on_shutdown.log")) +os.execute(string.format("grep -r \"join module fiber\" on_shutdown.log")) +os.execute(string.format("grep -r \"module_fiber_f finished\" \ + on_shutdown.log")) +os.execute(string.format("grep -r \"join module fiber finished\" \ + on_shutdown.log")) +test_run:cmd('start server test') +test_run:cmd('switch test') + +box.ctl.set_on_shutdown_timeout(1) +module = require('on_shutdownlib') +-- Setting shutdown timeout for main module fiber function. +-- This timeout > on_shutdown triggers timeout, so on_shutdown +-- trigger does not have time to complete. +shutdown_timeout = 10 +module.cfg(shutdown_timeout) + +test_run:cmd('switch default') +test_run:cmd('stop server test') +os.execute(string.format("rm %s", on_shutdownlib)) +os.execute(string.format("grep -r \"stop module fiber\" on_shutdown.log")) +os.execute(string.format("grep -r \"join module fiber\" on_shutdown.log")) +os.execute(string.format("grep -r \"SystemError timed out\" on_shutdown.log")) + +test_run:cmd('cleanup server test') +test_run:cmd('delete server test') diff --git a/test/app/on_shutdownlib.c b/test/app/on_shutdownlib.c new file mode 100644 index 0000000000000000000000000000000000000000..1569bffdcb5f895780d729687855330e61f2686a --- /dev/null +++ b/test/app/on_shutdownlib.c @@ -0,0 +1,126 @@ +#include <lua.h> +#include <lauxlib.h> +#include <lualib.h> +#include <luaconf.h> + +#include <module.h> +#include "../unit/unit.h" + +struct module { + /** + * Flag of the module state, May be -1, 0, 1 + * -1 means that module thread failed to start + * 0 means that module currently stop + * 1 menas that module currently running + */ + int is_running; + /** + * Module fiber + */ + struct fiber *fiber; + /** + * Time to sleep, before module fiber finished + */ + double timeout; +}; + +static struct module module; + +/** Shutdown function to test on_shutdown API */ +static int +on_shutdown_module_bad_func(void *arg) +{ + fail_unless(0); + return 0; +} + +/** Shutdown function to stop the main fiber of the module */ +static int +on_shutdown_module_stop_func(void *arg) +{ + fprintf(stderr, "stop module fiber\n"); + fiber_wakeup(module.fiber); + fprintf(stderr, "join module fiber\n"); + fiber_join(module.fiber); + fprintf(stderr, "join module fiber finished\n"); + return 0; +} + +/** Main module thread function. */ +static int +module_fiber_f(va_list arg) +{ + + __atomic_store_n(&module.is_running, 1, __ATOMIC_SEQ_CST); + /* + * In first yield we return control until the + * on_shutdown_module_stop_func function is called. + * Sleep required for test purpose: if sleep time > + * on_shutdown_wait_time then timeout in trigger_fiber_run + * will expire and tarantool will shutdown without waiting for the + * module to finish working. + */ + fiber_yield(); + fiber_sleep(module.timeout); + __atomic_store_n(&module.is_running, 0, __ATOMIC_SEQ_CST); + fprintf(stderr, "module_fiber_f finished\n"); + return 0; +} + +static int +cfg(lua_State *L) +{ + int index = lua_gettop(L); + if (index != 1) { + lua_pushstring(L, "fuction expected one argument"); + lua_error(L); + } + double timeout = luaL_checknumber(L, 1); + if (timeout < 0) { + lua_pushstring(L, "module shutdown sleep time must be greater " + "then or equal zero"); + lua_error(L); + } + + /** In case module already started do nothing */ + if (__atomic_load_n(&module.is_running, __ATOMIC_SEQ_CST) == 1) + return 0; + + module.timeout = timeout; + + int save_errno = errno; + /* + * Invalid function arguments: old_handle and new_hanlder can't + * be equal to zero at the same time. + */ + fail_unless(box_on_shutdown(&module, + NULL, + NULL) == -1); + fail_unless(errno == EINVAL); + errno = save_errno; + /** Registering the module stop function */ + fail_unless(box_on_shutdown(&module, + on_shutdown_module_bad_func, + NULL) == 0); + /** Changing the module stop function */ + fail_unless(box_on_shutdown(&module, + on_shutdown_module_stop_func, + on_shutdown_module_bad_func) == 0); + module.fiber = fiber_new("fiber", module_fiber_f); + fail_unless(module.fiber != NULL); + fiber_set_joinable(module.fiber, true); + fiber_start(module.fiber); + return 0; +} + +static const struct luaL_Reg on_shutdownlib[] = { + {"cfg", cfg}, + {NULL, NULL} +}; + +LUALIB_API int +luaopen_on_shutdownlib(lua_State *L) +{ + luaL_openlib(L, "on_shutdownlib", on_shutdownlib, 0); + return 0; +}