processors: close when construction fails partway#51687
Conversation
🤖 GitHub commentsJust comment with:
|
819bd56 to
998409d
Compare
Processor construction had three error paths that dropped already-constructed processors without calling Close, leaking any resources they held for the lifetime of the process: - processors.New: while building a list, a processor that failed to construct caused the function to return without closing the processors built earlier in the same list. - NewConditional: every registered processor is wrapped as NewConditional(SafeWrap(constructor)). The inner constructor runs first, then the `when` condition is parsed. A malformed condition returned an error but left the already-constructed processor open. - NewIfElseThenProcessor: if the `else` branch failed to build, the already-built `then` processors were dropped without Close. The fix closes the already-constructed processors on each of these error paths.
998409d to
b9a3538
Compare
|
Pinging @elastic/elastic-agent-data-plane (Team:Elastic-Agent-Data-Plane) |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (1)
👮 Files not reviewed due to content moderation or server errors (1)
📝 Walkthrough
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR prevents resource leaks when processor construction fails partway through building a processor chain in libbeat/processors, ensuring already-constructed processors are properly Close()d on error paths.
Changes:
- Close partially-constructed processor lists on
processors.Newfailures. - Close already-constructed processors when conditional parsing fails (
NewConditional) and whenif/then/elseconstruction fails while building theelsebranch. - Add unit tests covering two of the fixed error paths and include a changelog fragment.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| libbeat/processors/processor.go | Adds an abort helper to close already-built processors when list construction errors. |
| libbeat/processors/conditionals.go | Ensures constructed processors are closed if condition parsing or else branch construction fails. |
| libbeat/processors/conditionals_test.go | Adds tests asserting processors are closed on the new conditional and if/then/else error paths. |
| changelog/fragments/1783070100-close-processors-on-construction-errors.yaml | Adds a changelog entry documenting the leak fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
… error Cover the processors.New abort path directly: build a two-entry list where the first processor constructs successfully (a mock closer) and the second action does not exist, then assert the first processor was closed. The existing tests only exercised NewConditional and NewIfElseThenProcessor, leaving the list-level abort/Close logic in processor.go untested.
|
Tick the box to add this pull request to the merge queue (same as
|
|
@Mergifyio backport 9.4 9.3 8.19 |
✅ Backports have been createdDetails
|
The cherry-pick of #51687 introduced errors.Join calls in processor.go but the 8.19 branch did not have the "errors" stdlib import (main had it, 8.19 used github.com/joeshaw/multierror instead). This caused a build failure across all beats.
processors: close processors when construction fails partway Processor construction had three error paths that dropped already-constructed processors without calling Close, leaking any resources they held for the lifetime of the process: - processors.New: while building a list, a processor that failed to construct caused the function to return without closing the processors built earlier in the same list. - NewConditional: every registered processor is wrapped as NewConditional(SafeWrap(constructor)). The inner constructor runs first, then the `when` condition is parsed. A malformed condition returned an error but left the already-constructed processor open. - NewIfElseThenProcessor: if the `else` branch failed to build, the already-built `then` processors were dropped without Close. The fix closes the already-constructed processors on each of these error paths. (cherry picked from commit 1f95636) Co-authored-by: Orestis Floros <orestis.floros@elastic.co>
processors: close processors when construction fails partway Processor construction had three error paths that dropped already-constructed processors without calling Close, leaking any resources they held for the lifetime of the process: - processors.New: while building a list, a processor that failed to construct caused the function to return without closing the processors built earlier in the same list. - NewConditional: every registered processor is wrapped as NewConditional(SafeWrap(constructor)). The inner constructor runs first, then the `when` condition is parsed. A malformed condition returned an error but left the already-constructed processor open. - NewIfElseThenProcessor: if the `else` branch failed to build, the already-built `then` processors were dropped without Close. The fix closes the already-constructed processors on each of these error paths. (cherry picked from commit 1f95636) Co-authored-by: Orestis Floros <orestis.floros@elastic.co>
…tway (#51727) * processors: close when construction fails partway (#51687) processors: close processors when construction fails partway Processor construction had three error paths that dropped already-constructed processors without calling Close, leaking any resources they held for the lifetime of the process: - processors.New: while building a list, a processor that failed to construct caused the function to return without closing the processors built earlier in the same list. - NewConditional: every registered processor is wrapped as NewConditional(SafeWrap(constructor)). The inner constructor runs first, then the `when` condition is parsed. A malformed condition returned an error but left the already-constructed processor open. - NewIfElseThenProcessor: if the `else` branch failed to build, the already-built `then` processors were dropped without Close. The fix closes the already-constructed processors on each of these error paths. (cherry picked from commit 1f95636) * fix cherry-pick: add missing "errors" import to processor.go The cherry-pick of #51687 introduced errors.Join calls in processor.go but the 8.19 branch did not have the "errors" stdlib import (main had it, 8.19 used github.com/joeshaw/multierror instead). This caused a build failure across all beats. --------- Co-authored-by: Orestis Floros <orestis.floros@elastic.co>
Proposed commit message
Checklist
I have made corresponding changes to the documentationI have made corresponding change to the default configuration filesstresstest.shscript to run them under stress conditions and race detector to verify their stability../changelog/fragmentsusing the changelog tool.Disruptive User Impact
None. This only affects error paths during processor construction and does not change behavior for successfully built processors. The leaked resources are now released instead of being held for the process lifetime.
How to test this PR locally
Run the package tests, including the two new tests that assert the constructed processor is closed on each error path:
Reproduce the leak with a real Filebeat config
This needs no Docker.
add_docker_metadatastarts a background reconnect goroutine at construction that is stopped only byClose(); withwait_for_metadata_timeout: 0it retries forever. The emptywhencondition fails to build after that processor has already been constructed, so before this change the processor was dropped withoutClose()and its reconnect goroutine was orphaned for the lifetime of the process. The leak is an orphaned goroutine, so it is observed through the goroutine profile rather than through the config or the logs./tmp/fb-repro/filebeat.yml:Run filebeat, then read the goroutine profile:
On
mainthegrep -cprints1: the reconnect goroutine is orphaned and remains in the profile, with a parentless stack:With this change the constructed processor is closed on the error path and the count is
0.