Skip to content

fix(go.d/dyncfg): remove wait-decision timeout and make handoff non-droppable - #22201

Merged
ilyam8 merged 6 commits into
netdata:masterfrom
ilyam8:fix/go-dyncfg-no-drop
Apr 13, 2026
Merged

fix(go.d/dyncfg): remove wait-decision timeout and make handoff non-droppable#22201
ilyam8 merged 6 commits into
netdata:masterfrom
ilyam8:fix/go-dyncfg-no-drop

Conversation

@ilyam8

@ilyam8 ilyam8 commented Apr 13, 2026

Copy link
Copy Markdown
Member
Summary
Problem

When a collector's Check() is slow, the plugin spams false-positive warnings:

dyncfg: timed out waiting for enable/disable decision for 'X' ...

It's a race against the plugin's own single-threaded loop, not netdata being slow — the decision is already queued, the loop just can't read it in time.

Fix
  • Remove the wait-decision timeout (the warning's source).
  • Make the dyncfg command path non-droppable so an awaited enable/disable can't get silently lost and wedge the wait gate. Back-pressure now flows upstream via the OS pipe.

Note: this is a temporary fix. The proper solution is an architectural redesign that parallelizes Check() per job so the dyncfg pipeline isn't head-of-line blocked. That work is out of scope for this PR.

Test Plan
Additional Information
For users: How does this change affect me?

Summary by cubic

Remove the wait-decision timeout and make the dyncfg command path non-droppable, applying back-pressure instead of 503s. Also shrink the functions queue and change cancel handling to prevent wedged gates and noisy 499s.

  • Bug Fixes
    • Removed the wait-decision timer and its logs in Service Discovery and Job Manager; cleaned up timeout-based tests.
    • Made enqueueDyncfgFunction block with a simple channel send (no per-function timeout); now only returns 503 on shutdown.
    • Updated functions scheduler: enqueue blocks when full and wakes on next/cancelQueued/complete; manager stops the scheduler on context cancel to unblock waiters; default queue size set to 1.
    • Adjusted functions manager: removed the queue-full 503 path and queue_full_total metric; unexpected scheduler errors now log and return 500; queued CANCEL is ignored (function still runs); cancel fallback tombstones silently (no 499).
    • Tests: added SD regression asserting a second config blocks until the matching decision; made blocking-enqueue tests deterministic; updated flow/runtime tests for new cancel and queue semantics.

Written for commit 2c1359a. Summary will update on new commits.

…roppable

The 5s wait-decision timer in jobmgr and SD was a false-positive race
against the single-threaded run loop: when CmdEnable's Check() took ~5s
(reproduced with time.Sleep in snmp), unrelated discovered configs would
log "timed out waiting for enable/disable decision" even though their
decision command was already queued and would be processed shortly. The
loop just couldn't read it in time. Removed the timer.

Without the timer, an awaited enable/disable that gets dropped upstream
would wedge the wait gate forever. Closed every drop point in the dyncfg
chain so commands can no longer be silently lost:

- framework/functions scheduler.enqueue now blocks on cond.Wait when full
  instead of returning errSchedulerQueueFull (was: 503 to netdata).
  next/cancelQueued/complete broadcast cond so producers wake when space
  frees. A small ctx-watcher in Manager.run calls scheduler.stop() on
  ctx.Done so blocked enqueue waiters unblock during shutdown.
- jobmgr and SD enqueueDyncfgFunction now use a plain blocking channel
  send guarded only by the base context (was: BoundedSend with per-fn
  timeout -> 503). The per-fn timeout removal is intentional: it was the
  source of late drops that could starve the wait gate.

Back-pressure now propagates upstream via the OS pipe (stdin reader pauses
when the chain is jammed) instead of being absorbed into 503 responses.

Removed the now-dead queue_full_total runtime metric and updated the
framework README to reflect the blocking admission semantics. Tests that
exercised the wait-timeout / queue-full paths were removed or rewritten.

Out of scope: netdata-core "FUNCTION_RESULT_BEGIN ... transaction is not
found" logs caused by the 120s netdata transaction timeout still apply
when many commands queue behind a slow Check(). Tracked separately for a
follow-up that parallelizes Check() per job.
@github-actions github-actions Bot added area/docs area/collectors Everything related to data collection collectors/go.d area/go labels Apr 13, 2026

@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.

No issues found across 14 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.
Architecture diagram
sequenceDiagram
    participant ND as Netdata (Parent Process)
    participant RDR as Plugin Stdin Reader
    participant MGR as Functions Manager
    participant SCH as Keyed Scheduler
    participant WRK as Worker Pool
    participant SD as SD / Job Manager

    Note over ND, SD: Dynamic Configuration (dyncfg) & Function Execution Flow

    ND->>RDR: Send dyncfg command / Function call
    RDR->>MGR: dispatchInvocation()
    
    rect rgb(240, 240, 240)
        Note right of SCH: Back-pressure Mechanism
        MGR->>SCH: NEW: enqueue(request)
        alt Queue is Full
            SCH->>SCH: Block on sync.Cond
            Note over ND, SCH: Back-pressure: MGR blocks -> RDR stops reading -> OS Pipe fills -> ND write() blocks
        else Space Available or Slot Frees
            SCH-->>MGR: Request Accepted
        end
    end

    WRK->>SCH: next()
    SCH-->>WRK: Return task
    SCH->>SCH: CHANGED: Broadcast to unblock waiting enqueue()

    WRK->>WRK: Execute Check() / Function
    
    WRK->>SCH: complete(key, uid)
    SCH->>SCH: CHANGED: Broadcast to unblock waiting enqueue()

    Note over SD, SCH: dyncfg Wait Gate (Enable/Disable)
    SD->>MGR: CHANGED: enqueueDyncfgFunction() (non-droppable)
    MGR->>SCH: enqueue()
    
    Note over SD: NEW: Wait indefinitely for decision<br/>(Removed wait-decision timeout)
    
    alt Shutdown Signal
        MGR->>SCH: stop()
        SCH->>SCH: Broadcast to unblock all waiters
        SCH-->>MGR: return errSchedulerStopping
        MGR-->>ND: Send 503 (Shutting down)
    end
Loading

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.

Pull request overview

This PR removes the enable/disable “wait decision” timeout in dyncfg flows and changes the Go functions framework to apply back-pressure (blocking) instead of dropping/rejecting commands when internal queues are full, preventing false timeout warnings and wedged wait-gates.

Changes:

  • Removed wait-decision timeout handling/logging from job manager and service discovery, and updated tests accordingly.
  • Updated the functions scheduler to block on full capacity (non-droppable enqueue) and adjusted manager shutdown to unblock blocked enqueues on context cancel.
  • Removed the “queue full” runtime metric and updated documentation/tests for the new back-pressure behavior.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/go/plugin/framework/functions/scheduler.go Scheduler enqueue now blocks when full; condition signaling adjusted to wake producers/consumers.
src/go/plugin/framework/functions/scheduler_test.go Reworked scheduler tests to validate blocking enqueue semantics.
src/go/plugin/framework/functions/manager.go Adds a ctx-cancel watcher to stop the scheduler and unblock blocked enqueue during shutdown; updates enqueue error handling.
src/go/plugin/framework/functions/runtime_metrics.go Removes queue-full counter metric.
src/go/plugin/framework/functions/runtime_metrics_test.go Updates metrics scenario to reflect removal of queue-full behavior/metric.
src/go/plugin/framework/functions/manager_flow_test.go Removes the “queue full rejected with 503” flow test since enqueue no longer rejects.
src/go/plugin/framework/functions/README.md Documents the new back-pressure/blocking behavior and removes the queue-full metric from docs.
src/go/plugin/agent/jobmgr/manager.go Removes wait-decision timeout configuration and timeout warning log.
src/go/plugin/agent/jobmgr/dyncfg_handoff.go Changes dyncfg handoff to blocking send (non-droppable) with shutdown-only 503.
src/go/plugin/agent/jobmgr/dyncfg_collector_test.go Removes use of the removed wait-decision timeout constant.
src/go/plugin/agent/jobmgr/manager_process_test.go Removes tests and imports related to wait-decision timeout behavior.
src/go/plugin/agent/discovery/sd/sd.go Removes wait-decision timeout configuration and timeout warning log.
src/go/plugin/agent/discovery/sd/dyncfg_handoff.go Changes dyncfg handoff to blocking send (non-droppable) with shutdown-only 503.
src/go/plugin/agent/discovery/sd/wait_decision_test.go Removes timeout-based wait-gate tests; keeps enable-command path test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/go/plugin/framework/functions/scheduler_test.go Outdated
Comment thread src/go/plugin/framework/functions/scheduler.go Outdated
Comment thread src/go/plugin/framework/functions/manager.go Outdated
Comment thread src/go/plugin/agent/jobmgr/dyncfg_handoff.go Outdated
Comment thread src/go/plugin/agent/discovery/sd/dyncfg_handoff.go Outdated
Comment thread src/go/plugin/framework/functions/manager.go
Comment thread src/go/plugin/framework/functions/scheduler_test.go
ilyam8 and others added 2 commits April 13, 2026 21:37
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Added enqueueWaiters counter on keyScheduler (incremented around
cond.Wait inside enqueue()) and an enqueueWaiterCount() accessor.

Replaced the time.Sleep / time.After timing windows in the two new
TestKeyScheduler_EnqueueBlocksUntilSpace subtests with
require.Eventually on enqueueWaiterCount(), so the tests now wait for
the goroutine to actually reach the blocking wait before asserting,
instead of relying on wall-clock delays that can flake under load.
@ilyam8
ilyam8 requested a review from Copilot April 13, 2026 18:44
@ilyam8
ilyam8 marked this pull request as ready for review April 13, 2026 18:45

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.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/go/plugin/agent/jobmgr/manager_process_test.go:75

  • This file removed the wait-decision timeout test, but there’s no replacement test asserting the new intended behavior (no timeout: additional configs should remain blocked until an explicit enable/disable decision arrives, and the gate should not wedge). Adding a new scenario similar to the removed test—but unblocking via a real decision rather than a timeout—would better validate the new semantics.
func TestRunNotifyRunningJobs_TickOutsideLock(t *testing.T) {
	mgr := New(Config{PluginName: testPluginName})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/go/plugin/framework/functions/manager.go
Comment thread src/go/plugin/framework/functions/README.md Outdated
Comment thread src/go/plugin/agent/discovery/sd/wait_decision_test.go
- README: correct admission docs to mention both errors that the
  blocking enqueue path can return (503 stopping, 500 invalid).
- manager.go dispatchInvocation: log unexpected scheduler errors via
  Warningf and respond 500 with err detail instead of swallowing them
  as a generic 503. Default branch is unreachable today but should
  surface loudly if a new error variant is added.
- SD wait_decision_test: add regression case asserting that a second
  config sent while WaitingForDecision is true blocks until the
  matching enable arrives, and then proceeds. Guards against a
  regression that would let configs interleave or accidentally clear
  the gate.

@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.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/go/plugin/framework/functions/README.md">

<violation number="1" location="src/go/plugin/framework/functions/README.md:60">
P3: This documentation line incorrectly says the enqueue path returns 500 for malformed input. In code, malformed parser input is warn-and-continue, while 500 here is specifically for an invalid scheduler request.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/go/plugin/framework/functions/README.md
ilyam8 added 2 commits April 14, 2026 00:13
Two changes addressing the same root cause: dyncfg enable/disable
commands wedging in 'accepted' when CANCEL arrives before the function
reaches jobmgr.

1. defaultQueueSize: 64 -> 1 (kept as constant, not removed).
   Every downstream stage is single-threaded today (1 worker, 1 jobmgr
   loop, serial Check()), so the only thing a 64-deep scheduler buys us
   is wedge surface: more queued requests = more cancellation candidates.
   With queueSize=1 the stdin reader back-pressures earlier through the
   OS pipe instead of admitting work it cannot process. If real
   downstream concurrency is added later, raise this again.

2. requestCancellation: for stateQueued, ignore the cancel entirely
   (no cancelRequested flag, no ctx cancellation, no fallback timer, no
   tombstone). Reason: dyncfg commands carry side-effects that must
   reach jobmgr; without those side-effects the wait gate stays
   'accepted' forever (since the wait-decision timeout was removed).
   Setting cancelRequested would make startInvocation skip the handler;
   a fallback-timer'd tryFinalize would tombstone + remove from
   invState before the worker pulls it. Either path still wedges. The
   queued function now runs to completion as if no cancel happened;
   netdata already considers the transaction done (it 504'd before
   sending CANCEL), so the eventual terminal response just produces a
   benign "transaction not found" log on the netdata side.

Behavior for stateRunning / stateAwaitingResult is unchanged.

Updated the corresponding flow-scenario test to assert the new
contract: queued cancel does not emit 499, function still runs, normal
terminal response goes through.
The cancel fallback timer used to emit a 499 to netdata after
cancelFallbackDelay. But by the time CANCEL was sent, netdata had
already 504'd the transaction at its end and removed the inflight
entry, so any response we emit just produces a "transaction not found"
log on the netdata side. Pure noise.

Replaced the fallback's respUID call with a new markCancelled() helper
that does the same bookkeeping as tryFinalize (tombstone, lane advance
via scheduler.complete, remove from invState) but does NOT emit
anything. The handler still runs to completion; its eventual terminal
response is silently dropped via the tombstone, same as before.

Updated three flow-scenario tests to assert silent cancel:
- "running cancel fallback tombstones silently (no emit)"
- "repeated cancel for same uid is idempotent and silent"
- "running cancel drops late terminal response" (also asserts no 499)
@ilyam8
ilyam8 merged commit 30e29e5 into netdata:master Apr 13, 2026
150 of 151 checks passed
@ilyam8
ilyam8 deleted the fix/go-dyncfg-no-drop branch April 13, 2026 21:39
stelfrag pushed a commit to stelfrag/netdata that referenced this pull request Apr 14, 2026
…roppable (netdata#22201)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
(cherry picked from commit 30e29e5)
@stelfrag stelfrag mentioned this pull request Apr 14, 2026
Ferroin pushed a commit that referenced this pull request Apr 14, 2026
…roppable (#22201)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
(cherry picked from commit 30e29e5)
nedi-app Bot pushed a commit that referenced this pull request Apr 24, 2026
…roppable (#22201)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants