Skip to content

processors: close when construction fails partway#51687

Merged
orestisfl merged 3 commits into
elastic:mainfrom
orestisfl:processor-error-path-leaks
Jul 6, 2026
Merged

processors: close when construction fails partway#51687
orestisfl merged 3 commits into
elastic:mainfrom
orestisfl:processor-error-path-leaks

Conversation

@orestisfl

@orestisfl orestisfl commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Proposed commit message

libbeat/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.

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have made corresponding change to the default configuration files
  • I have added tests that prove my fix is effective or that my feature works. Where relevant, I have used the stresstest.sh script to run them under stress conditions and race detector to verify their stability.
  • I have added an entry in ./changelog/fragments using 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:

cd libbeat
go test -race -run 'TestConditionErrorClosesProcessor|TestIfElseThenErrorClosesThenProcessors' ./processors/...
go test -race ./processors/...

Reproduce the leak with a real Filebeat config

This needs no Docker. add_docker_metadata starts a background reconnect goroutine at construction that is stopped only by Close(); with wait_for_metadata_timeout: 0 it retries forever. The empty when condition fails to build after that processor has already been constructed, so before this change the processor was dropped without Close() 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.

mkdir -p /tmp/fb-repro/data
for i in $(seq 1 60); do printf 'log line %04d filler filler filler filler filler filler filler\n' "$i"; done > /tmp/fb-repro/log.txt

/tmp/fb-repro/filebeat.yml:

filebeat.inputs:
  - type: filestream
    id: leaking-input
    enabled: true
    paths:
      - /tmp/fb-repro/log.txt
    processors:
      - add_docker_metadata:
          host: "unix:///var/run/nonexistent-docker.sock"
          wait_for_metadata_timeout: 0
          when: {}

output.file:
  path: /tmp/fb-repro/out
  filename: filebeat-out

http.enabled: true
http.host: localhost
http.port: 5066
http.pprof.enabled: true

Run filebeat, then read the goroutine profile:

./filebeat -e -c /tmp/fb-repro/filebeat.yml \
  --path.home /tmp/fb-repro --path.data /tmp/fb-repro/data --strict.perms=false &
sleep 10
curl -s "http://localhost:5066/debug/pprof/goroutine?debug=1" | grep -c retryConnectToDocker

On main the grep -c prints 1: the reconnect goroutine is orphaned and remains in the profile, with a parentless stack:

1 @ ...
#	...add_docker_metadata.(*addDockerMetadata).retryConnectToDocker	add_docker_metadata.go:227
#	...startDockerConnectionRetry.func3				add_docker_metadata.go:187
#	sync.(*WaitGroup).Go.func1					waitgroup.go:258

With this change the constructed processor is closed on the error path and the count is 0.

@orestisfl orestisfl added bug Team:Elastic-Agent-Data-Plane Label for the Agent Data Plane team backport-active-all Automated backport with mergify to all the active branches labels Jul 2, 2026
@orestisfl orestisfl self-assigned this Jul 2, 2026
@botelastic botelastic Bot added needs_team Indicates that the issue/PR needs a Team:* label and removed needs_team Indicates that the issue/PR needs a Team:* label labels Jul 2, 2026
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🤖 GitHub comments

Just comment with:

  • run docs-build : Re-trigger the docs validation. (use unformatted text in the comment!)
  • /test : Run the Buildkite pipeline.

@orestisfl orestisfl changed the title libbeat/processors: close processors when construction fails partway processors: close when construction fails partway Jul 2, 2026
@orestisfl orestisfl force-pushed the processor-error-path-leaks branch from 819bd56 to 998409d Compare July 2, 2026 11:43
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.
@orestisfl orestisfl force-pushed the processor-error-path-leaks branch from 998409d to b9a3538 Compare July 2, 2026 11:44
@orestisfl orestisfl marked this pull request as ready for review July 2, 2026 11:44
@orestisfl orestisfl requested a review from a team as a code owner July 2, 2026 11:44
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

Pinging @elastic/elastic-agent-data-plane (Team:Elastic-Agent-Data-Plane)

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 422e2e7b-49aa-4004-b079-bb7f4f9a6037

📥 Commits

Reviewing files that changed from the base of the PR and between 2f63f0e and bb3b354.

📒 Files selected for processing (1)
  • libbeat/processors/conditionals_test.go
👮 Files not reviewed due to content moderation or server errors (1)
  • libbeat/processors/conditionals_test.go

📝 Walkthrough

[!WARNING]

Walkthrough skipped

File diffs could not be summarized.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 🛠️ Update Documentation

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.

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 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.New failures.
  • Close already-constructed processors when conditional parsing fails (NewConditional) and when if/then/else construction fails while building the else branch.
  • 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.

Comment thread changelog/fragments/1783070100-close-processors-on-construction-errors.yaml Outdated
Comment thread libbeat/processors/conditionals_test.go
orestisfl and others added 2 commits July 2, 2026 13:53
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.
@mergify

mergify Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@orestisfl orestisfl merged commit 1f95636 into elastic:main Jul 6, 2026
198 of 201 checks passed
@orestisfl orestisfl deleted the processor-error-path-leaks branch July 6, 2026 06:03
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@Mergifyio backport 9.4 9.3 8.19

@mergify

mergify Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

backport 9.4 9.3 8.19

✅ Backports have been created

Details

orestisfl added a commit that referenced this pull request Jul 6, 2026
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.
orestisfl added a commit that referenced this pull request Jul 6, 2026
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>
orestisfl added a commit that referenced this pull request Jul 6, 2026
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>
orestisfl added a commit that referenced this pull request Jul 6, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-active-all Automated backport with mergify to all the active branches bug Team:Elastic-Agent-Data-Plane Label for the Agent Data Plane team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants