fix(go.d/dyncfg): remove wait-decision timeout and make handoff non-droppable - #22201
Conversation
…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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
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.
There was a problem hiding this comment.
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.
- 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.
There was a problem hiding this comment.
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.
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)
…roppable (netdata#22201) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> (cherry picked from commit 30e29e5)
…roppable (#22201) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Summary
Problem
When a collector's
Check()is slow, the plugin spams false-positive warnings: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
Test Plan
Additional Information
For users: How does this change affect me?
Summary by cubic
Remove the wait-decision timeout and make the
dyncfgcommand path non-droppable, applying back-pressure instead of 503s. Also shrink thefunctionsqueue and change cancel handling to prevent wedged gates and noisy 499s.enqueueDyncfgFunctionblock with a simple channel send (no per-function timeout); now only returns 503 on shutdown.functionsscheduler:enqueueblocks when full and wakes onnext/cancelQueued/complete; manager stops the scheduler on context cancel to unblock waiters; default queue size set to 1.functionsmanager: removed the queue-full 503 path andqueue_full_totalmetric; unexpected scheduler errors now log and return 500; queuedCANCELis ignored (function still runs); cancel fallback tombstones silently (no 499).Written for commit 2c1359a. Summary will update on new commits.