Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/guides/applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ For Sample1, look for a log entry containing `sample1_module` and `PostInit`. Th

On macOS/Linux, Ctrl+C or `SIGTERM` requests shutdown through the application's main loop. On Windows, enter `exit` in the application console. The launcher first stops managed top-level task admission and continues module updates plus the shared owner-thread poll until accepted tasks finish. Their services and bridges remain available so those tasks can complete later awaits.

After task completion and the active callback stack returns, the manager synchronously invokes shutdown hooks and unloading. When all hooks succeed, Sample1 logs `PreShut` and then `Shut`, and the launcher exits. A hook may block; the handler-count budget is not a shutdown-duration bound.
After task completion and the active callback stack returns, the manager waits for every module to report `ready_to_shut()`, then synchronously invokes shutdown hooks and unloading. A module must use its owner-frame readiness check to stop/drain native work and confirm native thread exit before allowing unload. When all hooks succeed, Sample1 logs `PreShut` and then `Shut`, and the launcher exits. A hook may block; the handler-count budget is not a shutdown-duration bound.

If a hook returns `false` or throws, the launcher reports its phase, module and available exception details once, keeps the process/resources alive, and does not continue unloading. There is no implicit timeout or cancellation fallback. This managed-task drain does not prove that arbitrary callbacks, native pool workers or background cleanup have finished; see [module task integration](tasks.md#shared-business-executor-for-modules). Sample1's logs alone are not a readiness or shutdown guarantee for every plugin combination.
If a shutdown hook returns `false` or throws, the launcher reports its phase, module and available exception details once, keeps the process/resources alive, and does not continue unloading. `plugin_manager::update()` returning `false` or throwing during drain also locks the launcher into retention; owner updates continue where possible, but later completion cannot resume unloading.

`--shutdown_timeout_ms=N` configures a non-negative int64 wait in milliseconds; the default `0` waits indefinitely. A positive limit uses a monotonic clock and reports elapsed time, pending managed-task count and the waiting phase once. On expiry the process and resources stay alive, owner updates continue, and unloading remains disabled even if work completes later. This is a cooperative owner-loop wait limit: it cannot interrupt a blocking user `update()`, readiness check, shutdown hook or unload call. It is not a hard process-stop deadline, cancellation policy or successful shutdown. This managed-task drain does not prove that arbitrary callbacks, native pool workers or background cleanup have finished; see [module task integration](tasks.md#shared-business-executor-for-modules). Sample1's logs alone are not a readiness or shutdown guarantee for every plugin combination.

<Aside type="caution" title="Do not use process survival as the readiness check">

Manifest loading or validation failures return exit code `1` before plugin startup. If `plugin_manager::start()` returns `false`, the launcher reports a startup failure and returns `1` before entering the update loop or starting the Windows command thread. Completed startup work is not automatically rolled back. Argument parsing failures, including help handling, still return `0`. Check the expected module's initialization log and application-specific readiness before routing traffic to a process. A running PID or a zero exit status alone is insufficient.
Manifest loading or validation failures return exit code `1` before plugin startup. If `plugin_manager::start()` returns `false` or a module/plugin startup exception propagates, the launcher reports the failing phase, module or plugin and available exception details, then returns `1` before entering the update loop or starting the Windows command thread. The launcher does not roll back partial startup, run normal shutdown hooks or retry in the same process. Deployment tools should start a fresh process after this failure exit. Missing libraries or `DllEntryPlugin` entrypoints also report `load_plugin: <plugin>` and return `1`. An already loaded library missing its entrypoint is retained until process exit; it is not automatically unloaded. Argument parsing failures, including help handling, still return `0`. Check the expected module's initialization log and application-specific readiness before routing traffic to a process. A running PID or a zero exit status alone is insufficient.

</Aside>

Expand Down
8 changes: 5 additions & 3 deletions docs/guides/applications_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ Sample1 的日志出现 `sample1_module` 和 `PostInit`,表示示例已经进

macOS/Linux 可使用 Ctrl+C 或 `SIGTERM`,由应用主循环处理停止请求。Windows 在应用控制台输入 `exit`。启动器先停止受理新的顶层业务任务,继续执行模块更新与共享执行器轮询,直到已受理任务完成。任务使用的服务和桥接继续可用,以便协程完成后续等待。

任务完成且当前回调返回后,管理器同步执行关闭钩子与卸载。所有钩子成功时,Sample1 依次输出 `PreShut` 和 `Shut`,随后启动器退出。钩子可能阻塞;handler 数量预算不代表停机耗时上限。
任务完成且当前回调返回后,管理器等待所有模块的 `ready_to_shut()` 返回就绪,再同步执行关闭钩子与卸载。模块应在 owner frame 的就绪检查中停止并排空原生工作,确认原生线程退出后才允许卸载。所有钩子成功时,Sample1 依次输出 `PreShut` 和 `Shut`,随后启动器退出。钩子可能阻塞;handler 数量预算不代表停机耗时上限。

钩子返回 `false` 或抛出异常时,启动器输出一次阶段、模块及可用的异常信息,保留进程和资源,不再继续卸载。本实现没有隐含超时或自动取消的后备策略。业务任务排空不证明外部回调、原生连接池线程或后台清理已经完成,详见[模块任务集成](tasks_CN.md#模块共用业务执行器)。仅凭 Sample1 的日志,不能推断所有插件组合都具备相同的就绪条件或停止保证。
关闭钩子返回 `false` 或抛出异常时,启动器输出一次阶段、模块及可用的异常信息,保留进程和资源,不再继续卸载。排空期间的 `plugin_manager::update()` 返回 `false` 或抛出异常,也会锁定资源保留状态;启动器尽可能继续 owner 更新,但后续完成不能恢复卸载。

`--shutdown_timeout_ms=N` 设置等待时限,单位为毫秒,接受非负 int64 整数;默认值 `0` 表示无限等待。正数使用单调时钟计时,超时后输出一次实际等待时间、待完成的业务任务数和等待阶段。进程与资源继续保留,owner 更新继续运行;即使工作随后完成,也不再卸载。这是 owner loop 的协作式等待时限,不能中断阻塞中的用户 `update()`、就绪检查、关闭钩子或卸载调用,不构成强制进程退出期限、自动取消策略或成功停机。业务任务排空不证明外部回调、原生连接池线程或后台清理已经完成,详见[模块任务集成](tasks_CN.md#模块共用业务执行器)。仅凭 Sample1 的日志,不能推断所有插件组合都具备相同的就绪条件或停止保证。

<Aside type="caution" title="进程存在不等于应用就绪">

应用配置读取或校验失败时,启动器会在启动插件前返回退出码 `1`。`plugin_manager::start()` 返回 `false` 时,启动器会报告启动失败并返回 `1`,不进入更新循环,也不启动 Windows 命令线程;已经完成的启动工作不会自动回滚。参数解析失败(包括帮助处理)仍返回 `0`。接入流量前,应检查目标模块的初始化日志和应用自身的就绪条件,不能只判断 PID 存在或退出码为零。
应用配置读取或校验失败时,启动器会在启动插件前返回退出码 `1`。`plugin_manager::start()` 返回 `false`,或模块、插件启动异常向外传播时,启动器输出失败阶段、模块或插件名及可用异常信息,随后返回 `1`,不进入更新循环,也不启动 Windows 命令线程。启动器不会回滚部分启动工作、执行正常关闭钩子或在同一进程中重试;部署工具应在失败退出后启动新进程。缺少库文件或 `DllEntryPlugin` 入口时,也会报告 `load_plugin: <plugin>` 并返回 `1`。已加载但缺少入口的库保留至进程退出,不会自动卸载。参数解析失败(包括帮助处理)仍返回 `0`。接入流量前,应检查目标模块的初始化日志和应用自身的就绪条件,不能只判断 PID 存在或退出码为零。

</Aside>

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ Normal shutdown first stops managed top-level task admission and keeps pumping a

<Aside type="caution" title="Handle the manager's startup result">

The first `false` from `init()`, `post_init()`, `check_config()` or `pre_update()` stops that stage and all later startup stages, and `plugin_manager::start()` returns `false`. Each callback runs at most once in its stage. Modules that already completed a stage are not automatically rolled back.
The first `false` from `init()`, `post_init()`, `check_config()` or `pre_update()` stops that stage and all later startup stages, and `plugin_manager::start()` returns `false`. Each callback runs at most once in its stage. Exceptions propagate unchanged and also stop the remaining startup calls. `get_startup_failure()` retains the current `phase: module` context on failure, or `load_plugin: plugin` while loading a library and invoking its entrypoint; it is empty after successful startup and must be read on the manager's owner thread. Modules that already completed a stage are not automatically rolled back.

The application launcher checks this result and returns exit code `1` before its update loop or Windows command thread starts. This failure path does not call `pre_shut()` or `shut()` for partial-start cleanup. `update()` return values remain ignored. Keep the runtime plugin selection complete and check startup results in callers that manage their own loop.
The application launcher reports the retained context and any exception details, then returns exit code `1` before its update loop or Windows command thread starts. This failure path does not call `pre_shut()` or `shut()` for partial-start cleanup and does not retry in the same process; deployment tools should start a fresh process. Missing libraries and `DllEntryPlugin` entrypoints return false with `load_plugin: <plugin>` context. A loaded library missing its entrypoint remains retained until process exit because its static initializers may have started work; there is no automatic unload or rollback. Assertions for other program invariants remain unchanged. `update()` return values remain ignored. Keep the runtime plugin selection complete and check startup results in callers that manage their own loop.

</Aside>

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/modules_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ private:

<Aside type="caution" title="检查管理器的启动结果">

`init()`、`post_init()`、`check_config()` 或 `pre_update()` 首次返回 `false` 时,管理器停止该阶段的剩余模块及后续启动阶段,`plugin_manager::start()` 返回 `false`。同一阶段内每个回调至多执行一次;已经完成的阶段不会自动回滚。
`init()`、`post_init()`、`check_config()` 或 `pre_update()` 首次返回 `false` 时,管理器停止该阶段的剩余模块及后续启动阶段,`plugin_manager::start()` 返回 `false`。同一阶段内每个回调至多执行一次。异常保持原样向外传播,也会停止剩余启动调用。失败时,`get_startup_failure()` 保留当前的 `phase: module` 上下文;库加载及其入口调用使用 `load_plugin: plugin`。成功启动后上下文为空,读取时必须位于管理器所属线程。已经完成的阶段不会自动回滚。

应用启动器会检查这个结果,并在进入更新循环或启动 Windows 命令线程前返回退出码 `1`。这个失败路径不会调用 `pre_shut()` 或 `shut()` 清理部分启动资源。`update()` 的返回值仍被忽略。部署时应完整配置依赖插件;自行管理循环的调用方需要检查启动结果。
应用启动器输出保留的上下文及可用异常信息,并在进入更新循环或启动 Windows 命令线程前返回退出码 `1`。这个失败路径不会调用 `pre_shut()` 或 `shut()` 清理部分启动资源,也不会在同一进程中重试;部署工具应启动新进程。缺少库文件或 `DllEntryPlugin` 入口时,管理器返回 false,并保留 `load_plugin: <plugin>` 上下文。已加载但缺少入口的库保留至进程退出,因为其静态初始化可能已启动工作;管理器不会自动卸载或回滚。其他程序不变量的断言保持不变。`update()` 的返回值仍被忽略。部署时应完整配置依赖插件;自行管理循环的调用方需要检查启动结果。

</Aside>

Expand Down
36 changes: 30 additions & 6 deletions include/ark/plugin_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class plugin_manager final : private noncopyable
throw std::logic_error("Plugin manager cannot start during a lifecycle call or after shutdown begins");
}
phase_guard active(*this, call_phase::starting);
startup_failure_.clear();
timestamp_ = timestamp;

plugin_library_dir_ = lib_dir;
Expand Down Expand Up @@ -241,6 +242,11 @@ class plugin_manager final : private noncopyable
return pending_tasks_unchecked();
}

const std::string& get_startup_failure() const {
check_owner();
return startup_failure_;
}

shutdown_state get_shutdown_state() const {
check_owner();
return shutdown_state_;
Expand Down Expand Up @@ -445,8 +451,11 @@ class plugin_manager final : private noncopyable
for (auto const& iter : plugins_)
{
phase_guard installing(*this, call_phase::installing);
bool ret = load_plugin_library(iter.first);
ARK_ASSERT_RET_VAL(ret, false);
startup_failure_ = "load_plugin: " + iter.first;
if (!load_plugin_library(iter.first))
{
return false;
}
}

// initialize all modules
Expand All @@ -455,12 +464,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);

startup_failure_ = "init: " + iter.first;
if (!module_ptr->init())
{
return false;
}
}

startup_failure_.clear();
return true;
}

Expand All @@ -470,12 +481,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);

startup_failure_ = "post_init: " + iter.first;
if (!module_ptr->post_init())
{
return false;
}
}

startup_failure_.clear();
return true;
}

Expand All @@ -485,12 +498,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);

startup_failure_ = "check_config: " + iter.first;
if (!module_ptr->check_config())
{
return false;
}
}

startup_failure_.clear();
return true;
}

Expand All @@ -500,12 +515,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);

startup_failure_ = "pre_update: " + iter.first;
if (!module_ptr->pre_update())
{
return false;
}
}

startup_failure_.clear();
return true;
}

Expand Down Expand Up @@ -590,7 +607,13 @@ class plugin_manager final : private noncopyable
{
plugin_libs_.insert(std::make_pair(plugin_name, library));
auto func = reinterpret_cast<dll_entry_plugin_func>(library->get_symbol("DllEntryPlugin"));
ARK_ASSERT_RET_VAL(func != nullptr, false);
if (func == nullptr)
{
ARK_LOG_ERROR("Shared library[{}] has no DllEntryPlugin entrypoint", library->get_name());
// Keep an already loaded handle until process exit. Static
// initializers may have started work before symbol validation.
return false;
}
func(this, plugin_name, logger::instance());

return true;
Expand All @@ -603,10 +626,10 @@ class plugin_manager final : private noncopyable
auto error = dlerror();
#endif
ARK_LOG_ERROR("Load shared library[{}] failed, ErrorNo[{}]", library->get_name(), error);
ARK_ASSERT_RET_VAL(0, false);
// Native loading failed, so this object owns no loaded handle.
ARK_DELETE(library);
return false;
}

return true;
}

auto unload_plugin_library(const std::string& plugin_name) -> bool {
Expand Down Expand Up @@ -681,6 +704,7 @@ class plugin_manager final : private noncopyable
shutdown_state shutdown_state_{shutdown_state::running};
std::exception_ptr shutdown_exception_;
std::string shutdown_failure_;
std::string startup_failure_;

// Bus id
int bus_id_{0};
Expand Down
Loading
Loading