[tarantool-patches] [PATCH] Make `tarantoolctl start` work withiout box.cfg in init script

Max Melentiev dmarc-noreply at freelists.org
Fri Aug 9 11:10:22 MSK 2019


`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





More information about the Tarantool-patches mailing list