Skip to content

aclk/pluginsd: safely tear down hosts and preserve requested retries - #23867

Draft
stelfrag wants to merge 2 commits into
netdata:masterfrom
stelfrag:pluginsd-retry-after-forced-kill
Draft

stelfrag wants to merge 2 commits into
netdata:masterfrom
stelfrag:pluginsd-retry-after-forced-kill

Conversation

@stelfrag

Copy link
Copy Markdown
Collaborator
Summary
  • Drain queued ACLK commands before freeing host data, preventing use-after-free during host teardown and synchronization-loop shutdown.
  • Atomically close the ACLK command pool on shutdown, wake blocked producers, and complete pending cancellation/reset waiters.
  • Keep pluginsd retries enabled when a parser-requested restart requires a forced kill, rather than permanently disabling the collector.
  • Apply a longer retry backoff after a forced kill to avoid retry loops while the transient condition persists.

@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file

Architecture diagram
sequenceDiagram
    participant CD as Collector Daemon
    participant DS as Data Store
    participant WS as Worker Spawner
    participant PSR as Parser

    loop Worker Lifecycle
        CD->>WS: Spawn plugin process
        WS-->>CD: PID (unsafe.pi)
        CD->>PSR: Start parsing data
        PSR->>CD: retry request (transient condition)
        alt Retry requested
            CD->>WS: Request graceful exit (QUIT/SIGTERM)
            WS-->>CD: worker_ret_code
            alt worker_ret_code == -1 (forced kill)
                CD->>CD: Disable retry?<br/>No - keep retries enabled
                CD->>CD: Log warning<br/>Restart with longer backoff
                CD->>CD: Sleep update_every * 10
            else worker_ret_code != -1 (clean exit)
                CD->>CD: Sleep update_every
            end
            CD->>WS: Respawn plugin
        else No retry requested
            alt worker_ret_code == 0 (success)
                CD->>CD: Handle success
            else worker_ret_code != 0 (unexpected exit)
                CD->>CD: Handle failure/crash
            end
        end
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/plugins.d/plugins_d.c Outdated
Comment thread src/plugins.d/plugins_d.c Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/libnetdata/spawn_server/spawn_popen.h Outdated
Comment thread src/libnetdata/spawn_server/spawn_server_nofork.c Outdated
Comment thread src/libnetdata/spawn_server/spawn_server_libuv.c Outdated
Comment thread src/libnetdata/spawn_server/spawn_server.h Outdated
Comment thread src/libnetdata/spawn_server/spawn_server_windows.c Outdated
Comment thread src/libnetdata/spawn_server/spawn_server_posix.c Outdated
Comment thread src/plugins.d/plugins_d.c Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/libnetdata/spawn_server/spawn_server_windows.c Outdated
@stelfrag
stelfrag force-pushed the pluginsd-retry-after-forced-kill branch from f895fa4 to 365d7a9 Compare September 15, 2026 06:17
@stelfrag

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review PR

@stelfrag I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 9 files

Architecture diagram
sequenceDiagram
    participant WD as Worker Thread
    participant POP as spawn_popen
    participant SS as Spawn Server
    participant BP as Backend (POSIX/UV/Nofork/Windows)
    participant CFG as Config Parser

    Note over WD,CFG: Collector lifecycle & kill outcome tracking

    WD->>POP: spawn_popen_kill_ex(pi, 3s, &outcome)
    POP->>POP: Close file descriptors
    POP->>SS: spawn_server_exec_kill_ex(si, 3s, &outcome)
    
    alt Backend = POSIX/UV
        SS->>BP: Send SIGTERM
        BP-->>SS: Signal sent
        SS->>BP: spawn_server_exec_timedwait(grace_ms)
        alt Child exited within grace
            BP-->>SS: EXITED
            SS-->>POP: outcome = SPAWN_KILL_EXITED
        else Child still running
            SS->>BP: Send SIGKILL
            BP-->>SS: Signal sent
            SS->>BP: spawn_server_exec_timedwait(grace_ms)
            alt Child confirmed gone
                BP-->>SS: EXITED
                SS-->>POP: outcome = SPAWN_KILL_FORCED_EXITED
            else Still not confirmed
                BP-->>SS: TIMEDWAIT_TIMEOUT
                alt Deferred cleanup available
                    SS->>BP: Defer waitpid to reaper
                    SS-->>POP: outcome = SPAWN_KILL_UNKNOWN
                else No reaper slot
                    SS->>BP: spawn_server_exec_wait (unbounded)
                    BP-->>SS: Child reaped
                    SS-->>POP: outcome = SPAWN_KILL_FORCED_EXITED
                end
            end
        end
    else Backend = Windows
        SS->>BP: WaitForSingleObject(3s)
        alt Child exited within grace
            BP-->>SS: WAIT_OBJECT_0
            SS->>BP: TerminateChildProcesses
            SS->>BP: WaitForSingleObject(INFINITE)
            alt Handle valid
                BP-->>SS: WAIT_OBJECT_0
                SS-->>POP: outcome = SPAWN_KILL_EXITED
            else Handle failed
                BP-->>SS: WAIT_FAILED
                SS-->>POP: outcome = SPAWN_KILL_UNKNOWN
            end
        else Still running
            SS->>BP: kill(SIGTERM) + TerminateProcess
            SS->>BP: TerminateChildProcesses
            SS->>BP: WaitForSingleObject(INFINITE)
            alt Handle valid
                BP-->>SS: WAIT_OBJECT_0
                SS-->>POP: outcome = SPAWN_KILL_FORCED_EXITED
            else Handle failed
                BP-->>SS: WAIT_FAILED
                SS-->>POP: outcome = SPAWN_KILL_UNKNOWN
            end
        end
    else Backend = Nofork
        SS->>BP: Close fds, signal child
        alt Child exited
            BP-->>SS: EXITED
            SS-->>POP: outcome = SPAWN_KILL_EXITED
        else Need SIGKILL
            SS->>BP: Send SIGKILL
            BA-->>SS: Signal sent
            BP-->>SS: Timed wait
            alt Confirmed gone
                SS-->>POP: outcome = SPAWN_KILL_FORCED_EXITED
            else Not confirmed
                SS-->>POP: outcome = SPAWN_KILL_UNKNOWN
            end
        end
    end

    POP-->>WD: return code + outcome

    WD->>WD: Analyze kill_outcome

    alt SPAWN_KILL_UNKNOWN
        Note over WD: Process may still be running<br/>Refusing restart
        WD->>WD: Log error, disable plugin, set pid=0
    else SPAWN_KILL_EXITED or FORCED_EXITED
        alt retry == true (parser requested)
            WD->>WD: Log why (clean exit vs forced)
            WD->>WD: pluginsd_sleep_backoff(cd)
            WD->>WD: Restart collector
        else retry == false
            alt worker_ret_code == 0
                WD->>WD: Handle success, backoff before restart
            else error
                WD->>WD: Handle error, disable if needed, backoff
            end
        end
    end

    Note over CFG,WD: Config load with clamping
    CFG->>CFG: GET update_every from config
    CFG->>CFG: Clamp to [UPDATE_EVERY_MIN, UPDATE_EVERY_MAX]
    alt Out of range
        CFG->>CFG: Log warning, use clamped value
    end
    CFG-->>WD: update_every (now safe for backoff math)

    Note over WD: Backoff sleep uses usec_t<br/>avoids int overflow on large values
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/libnetdata/spawn_server/spawn_server_posix.c
Comment thread src/libnetdata/spawn_server/spawn_server_libuv.c
Comment thread src/libnetdata/spawn_server/spawn_server_libuv.c
A caller that must decide whether to start a replacement cannot learn what it
needs from the return code. spawn_popen_status_rc() maps every signal but
SIGTERM/SIGPIPE to -1, so our own SIGKILL, a crash and an OOM kill are one
value; on Windows a forced TerminateProcess() reports 0, like a clean exit. Two
backends also return without ever confirming the child died.

spawn_server_exec_kill_ex() and spawn_popen_kill_ex() report
SPAWN_KILL_{EXITED,FORCED_EXITED,UNKNOWN} alongside the status. The plain
functions remain as wrappers, so the other callers are untouched.

UNKNOWN is the one a caller must act on: the child may still be running, so a
replacement would write the same charts from two processes. It covers the posix
deferred-cleanup path, the nofork "child is left running" path, a libuv
uv_process_kill() failure, and a Windows handle that cannot be waited on -
WaitForSingleObject() returns WAIT_FAILED at once there, and the exit code that
follows was never set.

EXITED and FORCED_EXITED are a reporting distinction only. Neither says why the
child died: an operator and the OOM killer send the same SIGKILL we do, so no
test here can name a signal's sender, and the header says so to keep the next
caller from building policy on it.
When the parser asks for a retry it has hit a transient condition - a host being
deleted, a receiver it could not evict, a host it could not create - and wants a
fresh connection. The worker killed the plugin, read the -1 exit as abnormal and
disabled the collector permanently, which nothing undoes short of an agent
restart.

The retry is now honoured indefinitely, with no attrition. The plugin is not
what failed: go.d.plugin runs many independent jobs over one connection and the
retry conditions are per-vnode, so one device whose receiver cannot be evicted
aborts the session for all of them. Giving up on the plugin after a streak would
stop every healthy job in the process over one stuck device. The warning is
throttled per plugin, since the path has no end.

It does not ask why the child died, because that has no reliable answer - the
exit code collapses our SIGKILL, a crash and an OOM kill on POSIX, and inverts
on Windows where a forced termination looks like a clean exit. The one outcome
it acts on is SPAWN_KILL_UNKNOWN: not "why did it die" but "is it actually
gone", which is answerable, and respawning beside a live process would give two
writers on the same charts.

update_every is clamped where it is read and pluginsd_sleep() counts in usec_t:
the backoff could be zero for a legal "update every = 0", or overflow negative
for a large one, and all three backoff sites now share one helper.
@stelfrag
stelfrag force-pushed the pluginsd-retry-after-forced-kill branch from 365d7a9 to 6073267 Compare September 15, 2026 07:20
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The critical libuv cleanup issue must be resolved before approval.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR improves pluginsd process-kill reporting, retry handling, and backoff behavior. The described ACLK teardown changes are not included in the reviewed files.

Changes:

  • Adds kill outcome reporting across spawn backends.
  • Preserves parser-requested retries with longer backoff.
  • Clamps plugin intervals and retains compatibility wrappers.
File summaries
File Description
src/plugins.d/plugins_d.c Retry, backoff, and interval handling
src/libnetdata/spawn_server/spawn_server.h Kill outcome API
src/libnetdata/spawn_server/spawn_server_windows.c Windows kill reporting
src/libnetdata/spawn_server/spawn_server_posix.c POSIX kill reporting
src/libnetdata/spawn_server/spawn_server_nofork.c Nofork kill reporting
src/libnetdata/spawn_server/spawn_server_libuv.c Libuv kill outcomes
src/libnetdata/spawn_server/spawn_popen.h Public popen kill API
src/libnetdata/spawn_server/spawn_popen.c Popen wrapper implementation
src/libnetdata/spawn_server/spawn_library.c Compatibility wrapper

Review findings:

  • Critical (3 votes): Libuv termination can return without reclaiming the spawn instance or handle, potentially orphaning a live child.
  • Nit (1 vote each): The PR description claims ACLK teardown work absent from the reviewed changes.
Review details

Suppressed comments (2)

src/libnetdata/spawn_server/spawn_server_libuv.c:364

  • The PR description claims ACLK command-pool draining and host-teardown changes, but the PR's modified-file set contains no ACLK source; this diff only delivers the spawn/pluginsd changes. If ACLK safety is part of the acceptance criteria, it is missing from this PR; otherwise the description should be narrowed so it does not claim behavior this branch does not deliver.
    if (uv_process_kill(&si->process, SIGTERM)) {
        // Most often the child is already gone (ESRCH) and the exit callback simply has not been

src/plugins.d/plugins_d.c:184

  • The PR description claims that queued ACLK commands are drained and the ACLK command pool is closed during host teardown, but the supplied changes contain no ACLK command-pool or host-lifetime code; this hunk only adds spawn kill outcome handling. If those guarantees are part of this PR, that implementation is missing, otherwise the description should be corrected.
        SPAWN_KILL_OUTCOME kill_outcome;
        int worker_ret_code = spawn_popen_kill_ex(cd->unsafe.pi, 3 * MSEC_PER_SEC, &kill_outcome);
  • Files reviewed: 9/9 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 373 to 374
nd_log(NDLS_COLLECTORS, NDLP_ERR, "SPAWN PARENT: uv_process_kill() failed");
return -1;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants