From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from localhost (localhost [127.0.0.1]) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTP id DD2F925C7B for ; Fri, 9 Aug 2019 04:10:36 -0400 (EDT) Received: from turing.freelists.org ([127.0.0.1]) by localhost (turing.freelists.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id CAucaAqV1DVn for ; Fri, 9 Aug 2019 04:10:36 -0400 (EDT) Received: from smtp62.i.mail.ru (smtp62.i.mail.ru [217.69.128.42]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTPS id 6231825C2C for ; Fri, 9 Aug 2019 04:10:36 -0400 (EDT) From: "Max Melentiev" (Redacted sender "m.melentiev" for DMARC) Subject: [tarantool-patches] [PATCH] Make `tarantoolctl start` work withiout box.cfg in init script Date: Fri, 9 Aug 2019 11:10:22 +0300 Message-Id: <20190809081022.1237-1-m.melentiev@corp.mail.ru> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Sender: tarantool-patches-bounce@freelists.org Errors-to: tarantool-patches-bounce@freelists.org Reply-To: tarantool-patches@freelists.org List-Help: List-Unsubscribe: List-software: Ecartis version 1.0.0 List-Id: tarantool-patches List-Subscribe: List-Owner: List-post: List-Archive: To: tarantool-patches@freelists.org Cc: Max Melentiev `box.cfg` may not be called inside init script and tarantool will not be daemonized (forcing `background = true` in hijacked `box.cfg`), so we need to fork tarantool before running this script. To find out the moment when tarantool finishes startup it waits for `READY=1` message on NOTIFY_SOCKET which is emitted after init script is finished. Patch can be observed at https://github.com/tarantool/tarantool/pull/4381 It solves issue descrcibed here https://github.com/tarantool/tarantool/issues/4411 --- extra/dist/tarantoolctl.in | 141 +++++++++++++++++++++++++++++-------- 1 file changed, 113 insertions(+), 28 deletions(-) diff --git a/extra/dist/tarantoolctl.in b/extra/dist/tarantoolctl.in index 8adb57533..cdb601321 100755 --- a/extra/dist/tarantoolctl.in +++ b/extra/dist/tarantoolctl.in @@ -428,7 +428,9 @@ local cat_formats = setmetatable({ -- -------------------------------------------------------------------------- -- -- Commands -- -- -------------------------------------------------------------------------- -- -local orig_cfg = box.cfg +local orig_box_cfg = box.cfg +-- Last configured log device to fetch logs from +local configured_log_device local function wrapper_cfg(cfg) fiber.name(instance_name, {truncate=true}) @@ -464,23 +466,19 @@ local function wrapper_cfg(cfg) else cfg.username = nil end - if os.getenv("NOTIFY_SOCKET") then - cfg.background = false - elseif cfg.background == nil then - cfg.background = true - end + cfg.background = false mk_default_dirs(cfg) - local success, data = pcall(orig_cfg, cfg) - if not success then - log.error("Configuration failed: %s", data) - if type(cfg) ~= 'function' then - local log_type, log_args = logger_parse(cfg.log) - if log_type == 'file' and fio.stat(log_args) then - os.execute('tail -n 10 ' .. log_args) - end + configured_log_device = cfg.log + local data = orig_box_cfg(cfg) + + local box_cfg_mt = getmetatable(box.cfg) + local orig_box_cfg_call = box_cfg_mt.__call + box_cfg_mt.__call = function(old_cfg, new_cfg) + if old_cfg.pid_file ~= nil and new_cfg ~= nil and new_cfg.pid_file ~= nil then + new_cfg.pid_file = old_cfg.pid_file end - os.exit(1) + return orig_box_cfg_call(old_cfg, new_cfg) end return data @@ -506,6 +504,90 @@ local function start_check() return pid end +ffi.cdef([[ + pid_t fork(void); + int execve(const char *pathname, char *const argv[], char *const envp[]); + int dup2(int oldfd, int newfd); + int fileno(struct FILE *stream); +]]) + +local function ffi_table_to_const_char(input) + local result = ffi.new('char const*[?]', #input + 1, input) + result[#input] = nil + return ffi.cast('char *const*', result) +end + +-- Starts process and returns immediately, not waiting until process is finished. +-- @param path Executable path. +-- @param[opt] args +-- @param[opt] env +local function fork_execve(path, args, env) + args = args or {} + env = env or {} + table.insert(args, 1, path) + local argv = ffi_table_to_const_char(args) + local env_list = fun.iter(env):map(function(k, v) return k .. '=' .. v end):totable() + local envp = ffi_table_to_const_char(env_list) + local pid = tonumber(ffi.C.fork()) + if pid == -1 then + error('fork failed: ' .. errno.strerror()) + elseif pid > 0 then + return pid + end + ffi.C.execve(path, argv, envp) + io.stderr:write('execve failed: ' .. errno.strerror() .. '\n') + os.exit(1) +end + +-- Create socket and run forked tarantool with NOTIFY_SOCKET. +-- +-- `box.cfg` may not be called inside init script and tarantool +-- will not be daemonized (forcing `background = true` in hijacked `box.cfg`), +-- so we need to fork tarantool before running this script. +-- +-- To find out the moment when tarantool finishes startup it waits for `READY=1` +-- message on NOTIFY_SOCKET which is emitted after init script is finished. +local function fork_and_start_over() + local sock = assert(socket('AF_UNIX', 'SOCK_DGRAM', 0), 'Can not create socket') + local sock_name = '/tmp/tntctl-' .. require('uuid').str() + local ok = sock:bind('unix/', sock_name) + assert(ok, sock:error()) + fio.chown(sock_name, default_cfg.username, group_name) + fio.chmod(sock_name, tonumber('0770', 8)) + + local env = table.copy(os.environ()) + env.NOTIFY_SOCKET = sock_name + env.TARANTOOLCTL_DAEMONIZED = 'true' + -- slice 1..#arg indices into array-table + local args = fun.iter(arg):totable() + table.insert(args, 1, arg[0]) + local pid = fork_execve(arg[-1], args, env) + + local function exit(code) + fio.unlink(sock_name) + os.exit(code) + end + + local function check_process_alive() + if os.execute('ps -p ' .. pid .. ' > /dev/null') ~= 0 then + exit(1) + end + end + + local POLL_SOCKET_TIMEOUT = 1 -- sec. + while true do + check_process_alive() + if sock:readable(POLL_SOCKET_TIMEOUT) then + local str = sock:recv() + if str:match('READY=1') then + exit(0) + end + log.info(str) + end + check_process_alive() + end +end + local function start() log.info("Starting instance %s...", instance_name) if forward_to_systemd() then @@ -524,6 +606,10 @@ local function start() log.error("The daemon is already running: PID %s", pid) os.exit(1) end + if not os.getenv('NOTIFY_SOCKET') then + return fork_and_start_over() + end + box.cfg = wrapper_cfg require('title').update{ script_name = instance_path, @@ -539,26 +625,25 @@ local function start() -- if load fails - show last 10 lines of the log file and exit if not success then log.error("Start failed: %s", data) - if type(box.cfg) ~= 'function' then - local log_type, log_args = logger_parse(box.cfg.log) + local log_device = type(box.cfg) ~= 'function' and box.cfg.log or configured_log_device + if log_device then + local log_type, log_args = logger_parse(log_device) if log_type == 'file' and fio.stat(log_args) then os.execute('tail -n 10 ' .. log_args) end end os.exit(1) end - local box_cfg_mt = getmetatable(box.cfg) - if box_cfg_mt == nil then - log.error('box.cfg() is not called in an instance file') - os.exit(1) - end - local old_call = box_cfg_mt.__call - box_cfg_mt.__call = function(old_cfg, cfg) - if old_cfg.pid_file ~= nil and cfg ~= nil and cfg.pid_file ~= nil then - cfg.pid_file = old_cfg.pid_file - end - old_call(old_cfg, cfg) + + -- close descriptors in forked process after init script is finished + -- to show possible errors and logs. + if os.getenv('TARANTOOLCTL_DAEMONIZED') then + local fd = fio.open('/dev/null', fio.c.flag.O_RDONLY) + ffi.C.dup2(fd.fh, ffi.C.fileno(io.stdout)) + ffi.C.dup2(fd.fh, ffi.C.fileno(io.stderr)) + fd:close() end + return 0 end -- 2.21.0