From 7f6c1d6a6a6c7433c485de3045d206b796f12fa1 Mon Sep 17 00:00:00 2001 From: v Date: Tue, 31 Dec 2024 08:59:54 +0100 Subject: [PATCH] fixes and improvements based on analytics --- build/build.lua | 6 +- src/ami-plugin/asctl.lua | 50 +++++----- src/ascend.lua | 4 +- src/ascend/init.lua | 4 +- src/ascend/internals/env.lua | 107 +++++++++++--------- src/ascend/server.lua | 38 ++++---- src/ascend/services.lua | 182 ++++++++++++++++++----------------- src/ascend/tasks.lua | 12 +-- src/version-info.lua | 2 +- tests/common/test-env.lua | 3 +- 10 files changed, 216 insertions(+), 192 deletions(-) diff --git a/build/build.lua b/build/build.lua index 3e5397d..3fe3827 100644 --- a/build/build.lua +++ b/build/build.lua @@ -27,7 +27,7 @@ end local function minify(filePath) if not fs.exists("../build/luasrcdiet") then net.download_file("https://github.com/cryi/luasrcdiet/archive/refs/tags/1.1.1.zip", "../build/luasrcdiet.zip", - { followRedirects = true }) + { follow_redirects = true }) fs.mkdirp("../build/luasrcdiet") zip.extract("../build/luasrcdiet.zip", "../build/luasrcdiet", { flatten_root_dir = true }) end @@ -83,12 +83,12 @@ fs.mkdirp("../bin/ami/") local amiAsctlEntrypoint = "ami-plugin/asctl.lua" local amiAsctlOutput = "../bin/ami/asctl.lua" amalg("-o", amiAsctlOutput, "-s", amiAsctlEntrypoint, table.unpack(collect_requires(amiAsctlEntrypoint))) -local fileName = string.interpolate("${pluginName}-${version}.zip", { pluginName = "asctl", version = require"version-info".VERSION }) +local fileName = string.interpolate("${plugin_name}-${version}.zip", { plugin_name = "asctl", version = require"version-info".VERSION }) zip.compress("../bin/ami", path.combine("../bin", fileName), { recurse = true, content_only = true, overwrite = true }) -- minify -- if not fs.exists("../build/luasrcdiet") then --- net.download_file("https://github.com/cryi/luasrcdiet/archive/refs/tags/1.1.1.zip", "../build/luasrcdiet.zip", { followRedirects = true }) +-- net.download_file("https://github.com/cryi/luasrcdiet/archive/refs/tags/1.1.1.zip", "../build/luasrcdiet.zip", { follow_redirects = true }) -- fs.mkdirp("../build/luasrcdiet") -- zip.extract("../build/luasrcdiet.zip", "../build/luasrcdiet", { flatten_root_dir = true }) -- end diff --git a/src/ami-plugin/asctl.lua b/src/ami-plugin/asctl.lua index 82dd704..50ad2e5 100644 --- a/src/ami-plugin/asctl.lua +++ b/src/ami-plugin/asctl.lua @@ -11,15 +11,15 @@ local asctl = {} function asctl.exec(...) local cmd = string.join_strings(" ", ...) trace("Executing asctl " .. cmd) - local proc = proc.spawn("asctl", { ... }, { stdio = { stdout = "pipe", stderr = "pipe" }, wait = true }) --[[@as SpawnResult]] - if not proc then + local process = proc.spawn("asctl", { ... }, { stdio = { stdout = "pipe", stderr = "pipe" }, wait = true }) --[[@as SpawnResult]] + if not process then error("Failed to execute asctl command: " .. cmd) end - trace("asctl exit code: " .. proc.exit_code) + trace("asctl exit code: " .. process.exit_code) - local stderr = proc.stderrStream:read("a") - local stdout = proc.stdoutStream:read("a") - return proc.exit_code, stdout, stderr + local stderr = process.stderr_stream:read("a") + local stdout = process.stdout_stream:read("a") + return process.exit_code, stdout, stderr end function asctl.with_options(options) @@ -34,8 +34,8 @@ function asctl.install_service(sourceFile, serviceName, options) if type(options.kind) ~= "string" then options.kind = "service" end - local serviceUnitFile = string.interpolate("${serivceDirectory}/${service}.hjson", { - serivceDirectory = ASCEND_SERVICES, + local serviceUnitFile = string.interpolate("${service_directory}/${service}.hjson", { + service_directory = ASCEND_SERVICES, service = serviceName }) local _ok, _error = fs.safe_copy_file(sourceFile, serviceUnitFile) @@ -53,40 +53,40 @@ function asctl.install_service(sourceFile, serviceName, options) end end -function asctl.start_service(serviceName) - trace("Starting service: ${service}", { service = serviceName }) - local exit_code = asctl.exec("start", serviceName) +function asctl.start_service(service_name) + trace("Starting service: ${service}", { service = service_name }) + local exit_code = asctl.exec("start", service_name) assert(exit_code == 0, "Failed to start service") - trace("Service ${service} started.", { service = serviceName }) + trace("Service ${service} started.", { service = service_name }) end -function asctl.stop_service(serviceName) - trace("Stoping service: ${service}", { service = serviceName }) - local exit_code = asctl.exec("stop", serviceName) +function asctl.stop_service(service_name) + trace("Stoping service: ${service}", { service = service_name }) + local exit_code = asctl.exec("stop", service_name) assert(exit_code == 0, "Failed to stop service") - trace("Service ${service} stopped.", { service = serviceName }) + trace("Service ${service} stopped.", { service = service_name }) end -function asctl.remove_service(serviceName, options) +function asctl.remove_service(service_name, options) if type(options) ~= "table" then options = {} end - local serviceUnitFile = string.interpolate("${serivceDirectory}/${service}.hjson", { - serivceDirectory = ASCEND_SERVICES, - service = serviceName + local serviceUnitFile = string.interpolate("${service_directory}/${service}.hjson", { + service_directory = ASCEND_SERVICES, + service = service_name }) if not fs.exists(serviceUnitFile) then return end -- service not found so skip - trace("Removing service: ${service}", { service = serviceName }) - local exit_code = asctl.exec("stop", serviceName) + trace("Removing service: ${service}", { service = service_name }) + local exit_code = asctl.exec("stop", service_name) assert(exit_code == 0, "Failed to stop service") - trace("Service ${service} stopped.", { service = serviceName }) + trace("Service ${service} stopped.", { service = service_name }) trace("Removing service...") local ok, error = fs.safe_remove(serviceUnitFile) if not ok then error(string.interpolate("Failed to remove ${service} (${file}): ${error}", { - service = serviceName, + service = service_name, file = serviceUnitFile, error = error })) @@ -98,7 +98,7 @@ function asctl.remove_service(serviceName, options) warn({ msg = "Failed to reload ascend!", stdout = stdout, stderr = stderr }) end end - trace("Service ${service} removed.", { service = serviceName }) + trace("Service ${service} removed.", { service = service_name }) end function asctl.get_service_status(serviceName) diff --git a/src/ascend.lua b/src/ascend.lua index 2bab10f..5e4706e 100644 --- a/src/ascend.lua +++ b/src/ascend.lua @@ -34,11 +34,11 @@ tasks.add(services.healthcheck()) local end_time = timeout and os.time() + timeout or nil log_info("ascend started") -local stop_reason = tasks.run({ stopOnError = true , end_time = end_time }) +local stop_reason = tasks.run({ stop_on_error = true , end_time = end_time }) tasks.clear() tasks.add(services.stop_all()) -tasks.run({ ignoreStop = true, stopOnEmpty = true }) +tasks.run({ ignore_stop = true, stop_on_empty = true }) log_info("ascend stopped") diff --git a/src/ascend/init.lua b/src/ascend/init.lua index 5d7d2e9..8348dfb 100644 --- a/src/ascend/init.lua +++ b/src/ascend/init.lua @@ -9,7 +9,7 @@ local init = {} local function ami_init() local appsDir = os.getenv("ASCEND_APPS") if not appsDir then - appsDir = path.combine(path.dir(aenv.servicesDirectory), "apps") + appsDir = path.combine(path.dir(aenv.services_directory), "apps") end fs.mkdirp(appsDir) @@ -43,7 +43,7 @@ local commonInitStrategies = { local function run_init_hook() fs.mkdirp(aenv.logDirectory) - fs.mkdirp(aenv.servicesDirectory) + fs.mkdirp(aenv.services_directory) fs.mkdirp(aenv.healthchecksDirectory) if aenv.initScript ~= nil then diff --git a/src/ascend/internals/env.lua b/src/ascend/internals/env.lua index 9f4f148..d524c38 100644 --- a/src/ascend/internals/env.lua +++ b/src/ascend/internals/env.lua @@ -4,7 +4,7 @@ local input = require "common.input" local isUnix = package.config:sub(1, 1) == "/" local defaultAEnv = { - servicesDirectory = isUnix and "/etc/ascend/services" or "C:\\ascend\\services", + services_directory = isUnix and "/etc/ascend/services" or "C:\\ascend\\services", healthchecksDirectory = isUnix and "/etc/ascend/healthchecks" or "C:\\ascend\\healthchecks", ipcEndpoint = "/tmp/ascend.sock", logDirectory = isUnix and "/var/log/ascend" or "C:\\ascend\\logs", @@ -12,7 +12,7 @@ local defaultAEnv = { } local aenv = util.merge_tables({ - servicesDirectory = args.options.services or env.get_env("ASCEND_SERVICES"), + services_directory = args.options.services or env.get_env("ASCEND_SERVICES"), healthchecksDirectory = args.options.healthchecks or env.get_env("ASCEND_HEALTHCHECKS"), ipcEndpoint = args.options.socket or env.get_env("ASCEND_SOCKET"), logDirectory = args.options["log-dir"] or env.get_env("ASCEND_LOGS"), @@ -68,123 +68,141 @@ local function validate_service_definition(definition) if k == "all" then return false, "module name 'all' is reserved" end - local moduleInfo = { name = k } + local module_info = { name = k } if type(v) ~= "table" then - return false, string.interpolate("module ${name} must be an JSON object", moduleInfo) + local msg = string.interpolate("module ${name} must be an JSON object", module_info) + return false, msg end if type(v.executable) ~= "string" then - return false, string.interpolate("module ${name} - executable must be a string", moduleInfo) + local msg = string.interpolate("module ${name} - executable must be a string", module_info) + return false, msg end if type(v.args) ~= "table" then - return false, string.interpolate("module ${name} - args must be an array", moduleInfo) + local msg = string.interpolate("module ${name} - args must be an array", module_info) + return false, msg end if not table.is_array(v.depends) then - return false, string.interpolate("module ${name} - depends must be an array", moduleInfo) + local msg = string.interpolate("module ${name} - depends must be an array", module_info) + return false, msg end if type(v.restart) ~= "string" then - return false, string.interpolate("module ${name} - restart must be a string", moduleInfo) + local msg = string.interpolate("module ${name} - restart must be a string", module_info) + return false, msg end if not table.includes({ "always", "never", "on-failure", "on-success", "on-exit" }, v.restart) then - return false, - string.interpolate("module ${name} - restart must be one of: always, never, on-failure, on-success", - moduleInfo) + local msg = string.interpolate("module ${name} - restart must be one of: always, never, on-failure, on-success", module_info) + return false, msg end if type(v.restart_delay) ~= "number" then - return false, string.interpolate("module ${name} - restart_delay must be a number", moduleInfo) + local msg = string.interpolate("module ${name} - restart_delay must be a number", module_info) + return false, msg end if type(v.restart_max_retries) ~= "number" then - return false, string.interpolate("module ${name} - restart_max_retries must be a number", moduleInfo) + local msg = string.interpolate("module ${name} - restart_max_retries must be a number", module_info) + return false, msg end if v.stop_timeout and type(v.stop_timeout) ~= "number" then - return false, string.interpolate("module ${name} - stop_timeout must be a number or undefined", moduleInfo) + local msg = string.interpolate("module ${name} - stop_timeout must be a number or undefined", module_info) + return false, msg end if type(v.stop_signal) ~= "number" then - return false, string.interpolate("module ${name} - stop_signal must be a number", moduleInfo) + local msg = string.interpolate("module ${name} - stop_signal must be a number", module_info) + return false, msg end if type(v.environment) ~= "table" then - return false, string.interpolate("module ${name} - environment must be an JSON object", moduleInfo) + local msg = string.interpolate("module ${name} - environment must be an JSON object", module_info) + return false, msg end if type(v.working_directory) ~= "string" and type(v.working_directory) ~= "nil" then - return false, - string.interpolate("module ${name} - working_directory must be a string or undefined", moduleInfo) + local msg = string.interpolate("module ${name} - working_directory must be a string or undefined", module_info) + return false, msg end if type(v.working_directory) == "string" and #v.working_directory == 0 then - return false, string.interpolate("module ${name} - working_directory must not be empty", moduleInfo) + local msg = string.interpolate("module ${name} - working_directory must not be empty", module_info) + return false, msg end if type(v.log_file) == "string" and path.isabs(v.log_file) then local dir = path.dir(v.log_file) if not fs.exists(dir) then - return false, - string.interpolate("module ${name} - log_file directory ${dir} does not exist", - { name = k, dir = dir }) + local msg = string.interpolate("module ${name} - log_file directory ${dir} does not exist", + { name = k, dir = dir }) + return false, msg end end if type(v.log_rotate) ~= "boolean" then - return false, string.interpolate("module ${name} - log_rotate must be a boolean", moduleInfo) + local msg = string.interpolate("module ${name} - log_rotate must be a boolean", module_info) + return false, msg end if type(v.log_max_files) ~= "number" then - return false, string.interpolate("module ${name} - log_max_files must be a number", moduleInfo) + local msg = string.interpolate("module ${name} - log_max_files must be a number", module_info) + return false, msg elseif v.log_max_files < 0 then - return false, string.interpolate("module ${name} - log_max_files must be greater than 0", moduleInfo) + local msg = string.interpolate("module ${name} - log_max_files must be greater than 0", module_info) + return false, msg end local max_log_file_size = input.parse_size_value(tostring(v.log_max_size)) if type(max_log_file_size) ~= "number" then - return false, - string.interpolate("module ${name} - log_max_size must be a number (accepts k, m, g suffixes)", - moduleInfo) + local msg = string.interpolate("module ${name} - log_max_size must be a number (accepts k, m, g suffixes)", + module_info) + return false, msg elseif max_log_file_size < 1024 then - return false, - string.interpolate("module ${name} - log_max_size must be greater than 1KB", moduleInfo) + local msg = string.interpolate("module ${name} - log_max_size must be greater than 1KB", module_info) + return false, msg end if type(v.healthcheck) == "table" then -- healthchecks are optional so validate only if defined if type(v.healthcheck.name) ~= "string" then - return false, string.interpolate("module ${name} - healthcheck.name must be a string", moduleInfo) + local msg = string.interpolate("module ${name} - healthcheck.name must be a string", module_info) + return false, msg end if type(v.healthcheck.action) == "string" and not table.includes({ "restart", "none" }, v.healthcheck.action) then - return false, - string.interpolate("module ${name} - healthcheck.action must be one of: restart, none", moduleInfo) + local msg = string.interpolate("module ${name} - healthcheck.action must be one of: restart, none", module_info) + return false, msg end if type(v.healthcheck.interval) ~= "number" then - return false, string.interpolate("module ${name} - healthcheck.interval must be a number", moduleInfo) + local msg = string.interpolate("module ${name} - healthcheck.interval must be a number", module_info) + return false, msg end if type(v.healthcheck.timeout) ~= "number" then - return false, string.interpolate("module ${name} - healthcheck.timeout must be a number", moduleInfo) + local msg = string.interpolate("module ${name} - healthcheck.timeout must be a number", module_info) + return false, msg end if type(v.healthcheck.retries) ~= "number" then - return false, string.interpolate("module ${name} - healthcheck.retries must be a number", moduleInfo) + local msg = string.interpolate("module ${name} - healthcheck.retries must be a number", module_info) + return false, msg end if v.healthcheck.retries <= 0 then - return false, - string.interpolate("module ${name} - healthcheck.retries must be greater than 0", moduleInfo) + local msg = string.interpolate("module ${name} - healthcheck.retries must be greater than 0", module_info) + return false, msg end if type(v.healthcheck.delay) ~= "number" then - return false, string.interpolate("module ${name} - healthcheck.delay must be a number", moduleInfo) + local msg = string.interpolate("module ${name} - healthcheck.delay must be a number", module_info) + return false, msg end if v.healthcheck.interval < 1 then - return false, - string.interpolate("module ${name} - healthcheck.interval must be greater than 0", moduleInfo) + local msg = string.interpolate("module ${name} - healthcheck.interval must be greater than 0", module_info) + return false, msg end end end @@ -311,11 +329,12 @@ end ---@return table?, string? function aenv.load_service_definitions() - if fs.file_type(aenv.servicesDirectory) ~= "directory" then - return nil, string.interpolate("path ${path} is not a directory", { path = aenv.servicesDirectory }) + if fs.file_type(aenv.services_directory) ~= "directory" then + local msg = string.interpolate("path ${path} is not a directory", { path = aenv.services_directory }) + return nil, msg end - local defs = fs.read_dir(aenv.servicesDirectory, { recurse = false, return_full_paths = true, as_dir_entries = false }) --[=[@as string[]]=] + local defs = fs.read_dir(aenv.services_directory, { recurse = false, return_full_paths = true, as_dir_entries = false }) --[=[@as string[]]=] ---@type table local services = {} diff --git a/src/ascend/server.lua b/src/ascend/server.lua index 3ec09e8..6ff6815 100644 --- a/src/ascend/server.lua +++ b/src/ascend/server.lua @@ -9,7 +9,7 @@ local tasks = require "ascend.tasks" local server = {} ---@class ClientMessageBuffer ----@field msgLen number +---@field msg_length number ---@field msg string ---@type table @@ -83,19 +83,19 @@ local methodHandlers = { return end tasks.add(coroutine.create(function() - local responseData = {} + local response_data = {} local success = true ---@type thread[] local stopJobs = jobs.create_queue(jobs.array_to_array_of_params(request.params), function(name) local ok, err = services.stop(name, true) success = success and ok - responseData[name] = { + response_data[name] = { ok = ok, error = err } end) jobs.run_queue(stopJobs) - respond({ success = success, data = responseData }) + respond({ success = success, data = response_data }) end)) end, start = function(request, respond) @@ -108,19 +108,19 @@ local methodHandlers = { end tasks.add(coroutine.create(function() - local responseData = {} + local response_data = {} local success = true ---@type thread[] local startJobs = jobs.create_queue(jobs.array_to_array_of_params(request.params), function(name) local ok, err = services.start(name, { manual = true }) success = success and ok - responseData[name] = { + response_data[name] = { ok = ok, error = err } end) jobs.run_queue(startJobs) - respond({ success = success, data = responseData }) + respond({ success = success, data = response_data }) end)) end, restart = function(request, respond) @@ -133,19 +133,19 @@ local methodHandlers = { end tasks.add(coroutine.create(function() - local responseData = {} + local response_data = {} local success = true ---@type thread[] local restartJobs = jobs.create_queue(jobs.array_to_array_of_params(request.params), function(name) local ok, err = services.restart(name, { manual = true }) success = success and ok - responseData[name] = { + response_data[name] = { ok = ok, error = err } end) jobs.run_queue(restartJobs) - respond({ success = success, data = responseData }) + respond({ success = success, data = response_data }) end)) end, reload = function(request, respond) @@ -173,27 +173,27 @@ local methodHandlers = { end tasks.add(coroutine.create(function() - local responseData = {} + local response_data = {} local success = true ---@type thread[] local statusJobs = jobs.create_queue(jobs.array_to_array_of_params(request.params), function(name) local status, err = services.status(name) success = success and status ~= nil if not status then - responseData[name] = { + response_data[name] = { ok = false, error = err } return else - responseData[name] = { + response_data[name] = { ok = true, status = status } end end) jobs.run_queue(statusJobs) - respond({ success = success, data = responseData }) + respond({ success = success, data = response_data }) end)) end, logs = function(request, respond) @@ -263,22 +263,22 @@ function server.listen() return coroutine.create(function() local server, err = ipc.listen(aenv.ipcEndpoint, { accept = function(socket) - clients[socket] = { msgLen = 0, msg = "" } + clients[socket] = { msg_length = 0, msg = "" } end, data = function(socket, msg) local incomingLen = #msg if incomingLen == 0 then return end local clientBuffer = clients[socket] - if clientBuffer.msgLen == 0 then + if clientBuffer.msg_length == 0 then -- take first 4 bytes as length local len = encoding.decode_int(msg:sub(1, 4)) - clientBuffer.msgLen = len + clientBuffer.msg_length = len clientBuffer.msg = msg:sub(5) end - if clientBuffer.msgLen > 0 and clientBuffer.msgLen == #clientBuffer.msg then + if clientBuffer.msg_length > 0 and clientBuffer.msg_length == #clientBuffer.msg then local request, err = jsonrpc.parse_request(clientBuffer.msg) - clientBuffer.msgLen = 0 + clientBuffer.msg_length = 0 clientBuffer.msg = "" if err or request == nil then log_warn("failed to parse request: ${error}", { error = err or "unknown" }) diff --git a/src/ascend/services.lua b/src/ascend/services.lua index 2b1a8a9..5ed4c30 100644 --- a/src/ascend/services.lua +++ b/src/ascend/services.lua @@ -45,7 +45,7 @@ local log = require "ascend.log" ---@field stopped number? ---@type table -local managedServices = {} +local managed_services = {} local services = {} @@ -90,12 +90,12 @@ function services.init() for moduleName, moduleDefinition in pairs(definition.modules) do modules[moduleName] = new_managed_module(moduleDefinition) end - managedServices[name] = { + managed_services[name] = { modules = modules, source = definition.source } end - log_info("loaded ${count} services", { count = #table.keys(managedServices) }) + log_info("loaded ${count} services", { count = #table.keys(managed_services) }) return true end @@ -110,7 +110,7 @@ function services.reload() end for name, definition in pairs(definitions) do - local service = managedServices[name] or {} + local service = managed_services[name] or {} local modules = service.modules or {} for moduleName, moduleDefinition in pairs(definition.modules) do @@ -121,14 +121,14 @@ function services.reload() module.definition = moduleDefinition end end - managedServices[name] = { + managed_services[name] = { modules = modules, source = definition.source } end local to_remove = {} - for name in pairs(managedServices) do + for name in pairs(managed_services) do if not definitions[name] then table.insert(to_remove, name) end @@ -136,7 +136,7 @@ function services.reload() for _, name in ipairs(to_remove) do services.stop(name) - managedServices[name] = nil + managed_services[name] = nil end return true @@ -147,12 +147,12 @@ end ---@return table function services.list(services, extended) local list = {} - for name, _ in pairs(managedServices) do + for name, _ in pairs(managed_services) do if services and #services > 0 and not table.includes(services, name) then goto CONTINUE end list[name] = {} - for moduleName, module in pairs(managedServices[name].modules) do + for moduleName, module in pairs(managed_services[name].modules) do if extended then list[name][moduleName] = { state = module.state, @@ -173,29 +173,30 @@ end function services.show(names) local result = {} for _, name in ipairs(names) do - local serviceName, moduleName = name_to_service_module(name) - local service = managedServices[serviceName] + local service_name, module_name = name_to_service_module(name) + local service = managed_services[service_name] if not service then - return nil, string.interpolate("service ${name} not found", { name = serviceName }) + local msg = string.interpolate("service ${name} not found", { name = service_name }) + return nil, msg end local modules = service.modules - if moduleName ~= "all" then - modules = { [moduleName] = service.modules[moduleName] } + if module_name ~= "all" then + modules = { [module_name] = service.modules[module_name] } end local modulesResult = {} for moduleName, module in pairs(modules) do modulesResult[moduleName] = module.definition end - result[serviceName] = modulesResult + result[service_name] = modulesResult end return result end ---@class StartOptions ---@field manual boolean? ----@field isBoot boolean? +---@field is_boot boolean? ---@param module AscendManagedServiceModule ---@param options StartOptions? @@ -216,7 +217,7 @@ local function start_module(module, options) os.chdir(module.definition.working_directory) end - if not options.manual and not options.isBoot then + if not options.manual and not options.is_boot then module.restartCount = module.restartCount + 1 else -- if manually started, reset restart count module.restartCount = 0 @@ -275,25 +276,26 @@ function services.start(name, options) options = {} end - local serviceName, moduleName = name_to_service_module(name) - local service = managedServices[serviceName] + local service_name, module_name = name_to_service_module(name) + local service = managed_services[service_name] if not service then - return false, string.interpolate("service ${name} not found", { name = serviceName }) + local msg = string.interpolate("service ${name} not found", { name = service_name }) + return false, msg end - local modulesToManage = moduleName == "all" and service.modules or { [moduleName] = service.modules[moduleName] } + local modulesToManage = module_name == "all" and service.modules or { [module_name] = service.modules[module_name] } local modulesToManageCount = #table.keys(modulesToManage) if modulesToManageCount == 0 then - return false, - string.interpolate("module ${module} not found in service ${service}", - { module = moduleName, service = serviceName }) + local msg = string.interpolate("module ${module} not found in service ${service}", + { module = module_name, service = service_name }) + return false, msg end -- ---@type string[] -- local failedModules = {} -- local startedModules = 0 - for moduleName, managedModule in pairs(modulesToManage) do - if options.isBoot then + for module_name, managedModule in pairs(modulesToManage) do + if options.is_boot then if not managedModule.definition.autostart then -- if we are only starting auto-start modules, skip this module goto CONTINUE @@ -308,29 +310,29 @@ function services.start(name, options) if managedModule.definition.working_directory and not fs.exists(managedModule.definition.working_directory) then log_warn("working directory for ${name}:${module} does not exist", - { name = serviceName, module = moduleName }) + { name = service_name, module = module_name }) goto CONTINUE end - log_debug("starting ${name}:${module} - ${executable} from '${workingDirectory}'", + log_debug("starting ${name}:${module} - ${executable} from '${working_directory}'", { - name = serviceName, - workingDirectory = managedModule.definition.working_directory, + name = service_name, + working_directory = managedModule.definition.working_directory, executable = managedModule.definition.executable, - module = moduleName + module = module_name }) local ok, err = start_module(managedModule, options) if not ok then - log_debug("failed to start ${name}:${module} (${executable}) from '${workingDirectory}' - ${error}", + log_debug("failed to start ${name}:${module} (${executable}) from '${working_directory}' - ${error}", { - name = serviceName, - workingDirectory = managedModule.definition.working_directory, + name = service_name, + working_directory = managedModule.definition.working_directory, executable = managedModule.definition.executable, - module = moduleName, + module = module_name, error = err }) - log_warn("failed to start ${name}:${module}", { name = serviceName, module = moduleName }) + log_warn("failed to start ${name}:${module}", { name = service_name, module = module_name }) else - log_info("${name}:${module} started", { name = serviceName, module = moduleName }) + log_info("${name}:${module} started", { name = service_name, module = module_name }) end ::CONTINUE:: end @@ -358,28 +360,29 @@ end ---@param manual boolean? ---@return boolean, string? function services.stop(name, manual) - local serviceName, moduleName = name_to_service_module(name) - local service = managedServices[serviceName] + local service_name, module_name = name_to_service_module(name) + local service = managed_services[service_name] if not service then - return false, string.interpolate("${name} not found", { name = serviceName }) + local msg = string.interpolate("${name} not found", { name = service_name }) + return false, msg end - local modulesToStop = moduleName == "all" and service.modules or { [moduleName] = service.modules[moduleName] } + local modulesToStop = module_name == "all" and service.modules or { [module_name] = service.modules[module_name] } local modulesToManageCount = #table.keys(modulesToStop) if modulesToManageCount == 0 then - return false, - string.interpolate("module ${module} not found in service ${service}", - { module = moduleName, service = serviceName }) + local msg =string.interpolate("module ${module} not found in service ${service}", + { module = module_name, service = service_name }) + return false, msg end local stopJobs = {} - for moduleName, module in pairs(modulesToStop) do + for module_name, module in pairs(modulesToStop) do if module.state ~= "active" then goto CONTINUE end - log_debug("stopping ${service}:${module}", { service = serviceName, module = moduleName }) + log_debug("stopping ${service}:${module}", { service = service_name, module = module_name }) module.state = "stopping" table.insert(stopJobs, coroutine.create(function() @@ -401,7 +404,7 @@ function services.stop(name, manual) local exit_code = module.process:wait(1, 1000) if exit_code >= 0 then update_module_state_to_stopepd(module, exit_code, manual) - log_info("${service}:${module} stopped", { service = serviceName, module = moduleName }) + log_info("${service}:${module} stopped", { service = service_name, module = module_name }) return end coroutine.yield() @@ -413,22 +416,22 @@ function services.stop(name, manual) if not signalSent then log_debug("failed to send signal to ${service}:${module} - ${error}", - { service = serviceName, module = moduleName, error = err }) + { service = service_name, module = module_name, error = err }) end log_debug("${service}:${module} did not stop in time, killing it", - { service = serviceName, module = moduleName }) + { service = service_name, module = module_name }) -- force termination killTarget:kill(signal.SIGKILL) local exit_code = module.process:wait(10, 1000) if exit_code >= 0 then update_module_state_to_stopepd(module, exit_code, manual) - log_info("${service}:${module} stopped (killed)", { service = serviceName, module = moduleName }) + log_info("${service}:${module} stopped (killed)", { service = service_name, module = module_name }) return end - log_warn("failed to stop ${service}:${module}", { service = serviceName, module = moduleName }) + log_warn("failed to stop ${service}:${module}", { service = service_name, module = module_name }) end)) ::CONTINUE:: end @@ -440,7 +443,7 @@ end function services.stop_all() return coroutine.create(function() log_info("stopping all services") - local stopJobs = jobs.create_queue(jobs.array_to_array_of_params(table.keys(managedServices)), function(name) + local stopJobs = jobs.create_queue(jobs.array_to_array_of_params(table.keys(managed_services)), function(name) local ok, err = services.stop(name) if not ok then log_error(err --[[@as string]]) @@ -477,9 +480,10 @@ end ---@return table|false ---@return string? function services.status(name) - local service = managedServices[name] + local service = managed_services[name] if not service then - return false, string.interpolate("service ${name} not found", { name = name }) + local msg = string.interpolate("service ${name} not found", { name = name }) + return false, msg end local result = {} @@ -501,18 +505,18 @@ end function services.logs(name) log_debug("getting service ${name} log file", { name = name }) - local serviceName, moduleName = name_to_service_module(name) - local service = managedServices[serviceName] + local service_name, module_name = name_to_service_module(name) + local service = managed_services[service_name] if not service then - return nil, string.interpolate("service ${name} not found", { name = serviceName }) + return nil, string.interpolate("service ${name} not found", { name = service_name }) end - local modulesToGetLogsFor = moduleName == "all" and service.modules or { [moduleName] = service.modules[moduleName] } + local modulesToGetLogsFor = module_name == "all" and service.modules or { [module_name] = service.modules[module_name] } local modulesToGetLogsForCount = #table.keys(modulesToGetLogsFor) if modulesToGetLogsForCount == 0 then return nil, string.interpolate("module ${module} not found in service ${service}", - { module = moduleName, service = serviceName }) + { module = module_name, service = service_name }) end local logFiles = {} @@ -521,12 +525,12 @@ function services.logs(name) logFiles[moduleName] = module.__output_file:get_filename() end end - return serviceName, logFiles + return service_name, logFiles end function services.is_managed(name) local serviceName, moduleName = name_to_service_module(name) - local service = managedServices[serviceName] + local service = managed_services[serviceName] if not service then return false end @@ -540,9 +544,9 @@ end ---@return thread function services.manage(start) if start then - for name, _ in pairs(managedServices) do + for name, _ in pairs(managed_services) do log_debug("starting service ${name}", { name = name }) - local ok, err = services.start(name, { isBoot = true }) + local ok, err = services.start(name, { is_boot = true }) if not ok then log_error("failed to start service ${name}: ${error}", { name = name, error = err }) end @@ -551,8 +555,8 @@ function services.manage(start) return coroutine.create(function() while not is_stop_requested() do local time = os.time() - for serviceName, service in pairs(managedServices) do - for moduleName, module in pairs(service.modules) do + for service_name, service in pairs(managed_services) do + for module_name, module in pairs(service.modules) do if module.state ~= "to-be-started" then -- the log dir may not be created yet log.collect_output(module) end @@ -567,13 +571,13 @@ function services.manage(start) if module.state == "to-be-started" then if module.toBeStartedAt < time then - local ok, err = start_module(module, { isBoot = true }) + local ok, err = start_module(module, { is_boot = true }) if not ok then log_error("failed to start ${service}:${module} - ${error}", - { service = serviceName, module = moduleName, error = err }) + { service = service_name, module = module_name, error = err }) else log_debug("${service}:${module} started (delayed)", - { service = serviceName, module = moduleName }) + { service = service_name, module = module_name }) end end goto CONTINUE @@ -583,15 +587,15 @@ function services.manage(start) local exit_code = module.process:wait(1, 1000) if exit_code >= 0 then log_info("${service}:${module} exited with code ${code}", - { service = serviceName, module = moduleName, code = exit_code }) + { service = service_name, module = module_name, code = exit_code }) module.exit_code = exit_code module.state = exit_code == 0 and "stopped" or "failed" module.process = nil module.stopped = time elseif module.health.state == "unhealthy" and module.definition.healthcheck.action == "restart" then - log_debug("${service}:${module} is unhealthy", { service = serviceName, module = moduleName }) + log_debug("${service}:${module} is unhealthy", { service = service_name, module = module_name }) services.stop(string.interpolate("${service}:${module}", - { service = serviceName, module = moduleName })) + { service = service_name, module = module_name })) else goto CONTINUE -- if process is still running, skip the rest end @@ -610,18 +614,18 @@ function services.manage(start) shouldStart = true end if shouldStart then - log_debug("restarting ${service}:${module}", { service = serviceName, module = moduleName }) + log_debug("restarting ${service}:${module}", { service = service_name, module = module_name }) local ok, err = services.start(string.interpolate("${service}:${module}", - { service = serviceName, module = moduleName })) + { service = service_name, module = module_name })) if not ok then log_error("failed to restart ${service}:${module} - ${error}", - { service = serviceName, error = err }) + { service = service_name, error = err }) end end end if restartsExhausted and not module.notifiedRestartsExhausted then log_info("${service}:${module} has exhausted restarts", - { service = serviceName, module = moduleName }) + { service = service_name, module = module_name }) module.notifiedRestartsExhausted = true end ::CONTINUE:: @@ -632,17 +636,17 @@ function services.manage(start) end) end ----@param serviceName string ----@param moduleName string +---@param service_name string +---@param module_name string ---@param health AscendManagedServiceModuleHealth ----@param healthcheckDefinition AscendHealthCheckDefinition -local function run_healthcheck(serviceName, moduleName, health, healthcheckDefinition) +---@param healthcheck_definition AscendHealthCheckDefinition +local function run_healthcheck(service_name, module_name, health, healthcheck_definition) return coroutine.create(function() if health.isCheckInProgress then return end local lastChecked = health.lastChecked - local interval = healthcheckDefinition.interval + local interval = healthcheck_definition.interval local timeToCheck = lastChecked + interval < os.time() if not timeToCheck then return @@ -650,16 +654,16 @@ local function run_healthcheck(serviceName, moduleName, health, healthcheckDefin health.isCheckInProgress = true - local timeout = healthcheckDefinition.timeout + local timeout = healthcheck_definition.timeout local startTime = os.time() log_trace("running healthcheck ${name} for ${service}:${module}", - { name = healthcheckDefinition.name, service = serviceName, module = moduleName }) - local ok, proc = proc.safe_spawn(path.combine(aenv.healthchecksDirectory, healthcheckDefinition.name), + { name = healthcheck_definition.name, service = service_name, module = module_name }) + local ok, proc = proc.safe_spawn(path.combine(aenv.healthchecksDirectory, healthcheck_definition.name), { stdio = "inherit" }) if not ok then log_error("failed to run healthcheck for ${service}:${module} - ${error}", - { service = serviceName, module = moduleName, error = proc }) + { service = service_name, module = module_name, error = proc }) health.state = "unhealthy" health.lastChecked = os.time() health.isCheckInProgress = false @@ -668,7 +672,7 @@ local function run_healthcheck(serviceName, moduleName, health, healthcheckDefin while not is_stop_requested() and health.isCheckInProgress do if timeout > 0 and os.time() - startTime > timeout then - log_info("healthcheck for ${service}:${module} timed out", { service = serviceName, module = moduleName }) + log_info("healthcheck for ${service}:${module} timed out", { service = service_name, module = module_name }) health.state = "unhealthy" health.lastChecked = os.time() health.isCheckInProgress = false @@ -681,9 +685,9 @@ local function run_healthcheck(serviceName, moduleName, health, healthcheckDefin goto CONTINUE elseif exit_code == 0 then health.state = "healthy" - elseif health.unhealthyCheckCount + 1 >= healthcheckDefinition.retries then + elseif health.unhealthyCheckCount + 1 >= healthcheck_definition.retries then log_info("healthcheck for ${service}:${module} failed with exit code ${code}", - { service = serviceName, module = moduleName, code = exit_code }) + { service = service_name, module = module_name, code = exit_code }) health.state = "unhealthy" else health.unhealthyCheckCount = health.unhealthyCheckCount + 1 @@ -702,7 +706,7 @@ function services.healthcheck() local healthCheckJobs = {} while not is_stop_requested() do - for serviceName, service in pairs(managedServices) do + for serviceName, service in pairs(managed_services) do for moduleName, module in pairs(service.modules) do if module.state ~= "active" then goto CONTINUE @@ -736,7 +740,7 @@ end ---@return "healthy" | "unhealthy" function services.get_ascend_health(strict) local allHealthy = true - for _, service in pairs(managedServices) do + for _, service in pairs(managed_services) do for _, module in pairs(service.modules) do if strict and module.state ~= "active" then allHealthy = false diff --git a/src/ascend/tasks.lua b/src/ascend/tasks.lua index 0512c84..a7da711 100644 --- a/src/ascend/tasks.lua +++ b/src/ascend/tasks.lua @@ -1,9 +1,9 @@ local is_stop_requested = require "ascend.signal" ---@class TaskPoolOptions ----@field stopOnEmpty boolean? ----@field stopOnError boolean? ----@field ignoreStop boolean? +---@field stop_on_empty boolean? +---@field stop_on_error boolean? +---@field ignore_stop boolean? ---@field end_time number? local taskQueue = {} @@ -38,7 +38,7 @@ function tasks.run(options) end while true do - if not options.ignoreStop and is_stop_requested() then + if not options.ignore_stop and is_stop_requested() then return "requested" end if timed_out(options.end_time) then @@ -55,7 +55,7 @@ function tasks.run(options) local ok, err = coroutine.resume(task) if not ok then log_error("!!! task failed !!!", { error = err }) - if options.stopOnError then + if options.stop_on_error then error_occured = true break end @@ -68,7 +68,7 @@ function tasks.run(options) if error_occured then return "error" end - if (options.stopOnEmpty and #taskQueue == 0) then + if (options.stop_on_empty and #taskQueue == 0) then return "empty" end os.sleep(100, 1000) diff --git a/src/version-info.lua b/src/version-info.lua index 466c08f..bb05d16 100644 --- a/src/version-info.lua +++ b/src/version-info.lua @@ -1,4 +1,4 @@ -local ASCEND_VERSION = "0.7.4" +local ASCEND_VERSION = "0.7.5" return { VERSION = ASCEND_VERSION diff --git a/tests/common/test-env.lua b/tests/common/test-env.lua index beafd67..3a2ef46 100644 --- a/tests/common/test-env.lua +++ b/tests/common/test-env.lua @@ -69,7 +69,8 @@ local function patch_env(env, vars) return patch_env(val, vars) end if type(val) == "string" then - return string.interpolate(val, vars) + local msg = string.interpolate(val, vars) + return msg end return val end)