SIGN IN SIGN UP

fix: make module stop() callbacks signal-only so modules do not deadlock each other on shutdown

stop_wmodules() stopped and joined one module at a time, and three modules'
stop() callbacks were not lightweight signals: each blocked for up to 10 s
waiting for its own run loop to finish tearing down. agent-info's run loop can
be parked inside Syscollector::pause(), which only releases once syscollector
is told to stop, so agent-info burned its whole timeout before syscollector had
even been signalled. It then logged "Timeout waiting for AgentInfo run loop to
exit; skipping database teardown" and left its DBSync connection open.

Those waits were redundant. syscollector and SCA already run their teardown on
the module thread just before it returns, so joining that thread carries the
same guarantee, under one shared budget instead of a per-module timeout.
agent-info was the asymmetric one and now matches: the teardown runs from
wm_agent_info_main on every exit path, including the early return during the
handshake wait, where agent_info_task_registry_init() may already have opened
agent_info.db. It releases the module's connections rather than destroying the
instance, because the dispatcher and /control threads reach the module through
pointers they only null-check.

stop_wmodules() now signals every module and only then joins them all against a
single deadline, mirroring the POSIX SIGTERM handler. Signalling everything up
front makes it far more likely for a stop to arrive before a module has
finished starting, so the places that silently dropped it are fixed too:
AgentInfoImpl::start() no longer clears the stop and consults the injected
global-shutdown predicate rather than m_stopped alone (agent_info_stop()
null-checks the instance, so a stop arriving before it exists never reaches
stop() at all, and with no shouldContinue supplied the predicate is the run
loop's only exit condition), and SCA no longer resets g_shutting_down over a
stop already requested.

syscollector drops it too, in wm_sys_stop()'s !flags.running early return, and
there the fix is not to start the module at all: win_module_thread() declines to
run its routine when wm_shutdown_requested is already set on return from
startup_gate_wait_for_ready(), which the gate does on shutdown. Without that
check, 8 of 16 stops issued 1-2 s after start on Windows 11 burned the whole
20 s join budget and the successor process logged "sqlite: database is locked" --
the symptoms of #38370 and #38372. It is a stopgap: the gate itself should
report why it returned so that every caller can abort, which is #38428.

Also stop reporting a join timeout without checking the thread: once the shared
budget was spent WaitForSingleObject was skipped entirely and the error was
logged for modules that had already exited cleanly.

Three more shutdown-window defects found while reviewing this change. wm_sys_stop()
now records the stop before its !flags.running early return, and wm_sys_main()
refuses to initialise under a stop already received: both Syscollector::init() and
::start() clear m_stopping, so such a stop was erased and the module scanned on
through the shutdown. It also reads syscollector_stop_ptr into a local before
calling through it, since the module thread clears that global as it exits. The
join pass skips the calling thread, as the POSIX handler does with pthread_self():
merror_exit() from a module thread reaches stop_wmodules() through WinSetError(),
and waiting on its own handle burned the whole budget on a timeout that never
happened. And the budget now starts before the signal pass, because quiesce() waits
for an in-flight flush with no timeout of its own and that time was off the clock,
leaving the total unbounded against WaitToKillServiceTimeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
N
nbertoldo committed
b7b8fafda0dd5f5ad8e442d774f21ce313092534
Parent: 649266c