Skip to content

filestream: encode bridging raw header only for dedup winners#51914

Open
orestisfl wants to merge 2 commits into
elastic:mainfrom
orestisfl:filestream-bridging-raw-dedup-winners
Open

filestream: encode bridging raw header only for dedup winners#51914
orestisfl wants to merge 2 commits into
elastic:mainfrom
orestisfl:filestream-bridging-raw-dedup-winners

Conversation

@orestisfl

@orestisfl orestisfl commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Proposed commit message

In growing-fingerprint mode, fileScanner.toFileDescriptor hex-encoded
the raw fingerprint window for every complete file on every scan. The
completedFingerprints suppression set is rebuilt by fileWatcher.watch
from the post-dedup result only, so dedup losers can never enter it.
Every duplicate therefore re-read and re-encoded its ~2*length-byte raw
header on every scan forever, even though its descriptor is discarded
immediately at the dedup check.

Move the bridging-raw encode out of toFileDescriptor into a new
attachBridgingRaw helper that GetFiles calls only after the dedup-winner
decision. Duplicates never pay the encode, not even on their first scan.

Behavior for the winner is byte-identical to before: dedup uses FileID()
and every consumer of Fingerprint.Raw on a complete descriptor only ever
sees post-dedup descriptors. The bridging header is still attached on
the scan a file crosses the threshold, so rename/growth prefix matching
is unchanged.

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.

How to test this PR locally

Unit tests and benchmark:

cd filebeat
go test ./input/filestream/ -count=1 -run 'TestGetFiles_DuplicateFingerprint|TestGetFiles_GrowingRawSuppression|TestToFileDescriptor_GrowingLifecycle'
go test ./input/filestream/ -run '^$' -bench 'GetFilesWithFingerprintGrowing' -benchmem

End-to-end reproduction with filebeat:

  1. Generate 10,000 identical, above-threshold, aged files:
REPRO=/tmp/growing-repro
mkdir -p "$REPRO/logs" "$REPRO/home" "$REPRO/profiles"
head -c 2048 /dev/zero | tr '\0' 'a' > "$REPRO/content.txt"
for i in $(seq 1 10000); do cp "$REPRO/content.txt" "$REPRO/logs/file-$i.log"; done
touch -d '48 hours ago' "$REPRO/logs/"*.log   # aged past ignore_older
  1. filebeat.yml:
filebeat.inputs:
  - type: filestream
    id: growing-repro
    paths:
      - /tmp/growing-repro/logs/*.log
    prospector.scanner.check_interval: 1s
    ignore_older: 24h

queue.mem:
  events: 512
  flush.min_events: 1

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

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

logging.level: info
logging.to_stderr: true
  1. Run, then diff two allocs profiles ~45s apart:
./filebeat -e --strict.perms=false -c /tmp/growing-repro/filebeat.yml --path.home=/tmp/growing-repro/home &

curl -s 'http://localhost:5066/debug/pprof/allocs' -o /tmp/growing-repro/profiles/1.pb.gz
sleep 45
curl -s 'http://localhost:5066/debug/pprof/allocs' -o /tmp/growing-repro/profiles/2.pb.gz

go tool pprof -base /tmp/growing-repro/profiles/1.pb.gz -top -sample_index=alloc_space \
  /tmp/growing-repro/profiles/2.pb.gz
go tool pprof -base /tmp/growing-repro/profiles/1.pb.gz \
  -list 'toFileDescriptor$|attachBridgingRaw$' -sample_index=alloc_space \
  /tmp/growing-repro/profiles/2.pb.gz

Expected: all 9,999 duplicates dedup to the single winner (files_unique stays 1). On main, the ~45s window allocates ~2.5 GB with encoding/hex.EncodeToString accounting for ~1.8 GB (71%), all attributed to toFileDescriptor. With this PR the window drops to ~0.8 GB, hex.EncodeToString falls to ~66 MB (only the 64-char SHA-256 Sum for all files), and attachBridgingRaw does not appear in the diff because only the single winner encodes its raw header.

Logs

alloc_space diff over a ~45s window, before vs after (10,000 identical files, check_interval: 1s):

# BEFORE (main behavior: encode in toFileDescriptor for every complete file)
Showing nodes accounting for 2361.33MB, 92.85% of 2543.21MB total
      flat  flat%   sum%        cum   cum%
 1812.43MB 71.27% 71.27%  1812.43MB 71.27%  encoding/hex.EncodeToString (inline)
   ...
   32.50MB  1.28% 92.85%  1940.44MB 76.30%  (*fileScanner).toFileDescriptor

# AFTER (this PR: attachBridgingRaw only for dedup winners)
Showing nodes accounting for 790.60MB, 97.55% of 810.49MB total
      flat  flat%   sum%        cum   cum%
   ...
   65.50MB  8.08% 60.27%    65.50MB  8.08%  encoding/hex.EncodeToString
   26.50MB  3.27% 91.01%   185.51MB 22.89%  (*fileScanner).toFileDescriptor
# attachBridgingRaw does not appear: only the single winner encodes.

BenchmarkGetFilesWithFingerprintGrowing/duplicates (1,000 identical files, -benchmem), before vs after the encode move:

before:  ~5.29 MB/op   22052 allocs/op
after:   ~1.19 MB/op   20049 allocs/op

@orestisfl orestisfl self-assigned this Jul 14, 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 14, 2026
@github-actions

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.

In growing-fingerprint mode, fileScanner.toFileDescriptor hex-encoded
the raw fingerprint window for every complete file on every scan. The
completedFingerprints suppression set is rebuilt by fileWatcher.watch
from the post-dedup result only, so dedup losers can never enter it.
Every duplicate therefore re-read and re-encoded its ~2*length-byte raw
header on every scan forever, even though its descriptor is discarded
immediately at the dedup check.

Move the bridging-raw encode out of toFileDescriptor into a new
attachBridgingRaw helper that GetFiles calls only after the dedup-winner
decision. Duplicates never pay the encode, not even on their first scan.

Behavior for the winner is byte-identical to before: dedup uses FileID()
and every consumer of Fingerprint.Raw on a complete descriptor only ever
sees post-dedup descriptors. The bridging header is still attached on
the scan a file crosses the threshold, so rename/growth prefix matching
is unchanged.
@orestisfl orestisfl force-pushed the filestream-bridging-raw-dedup-winners branch from baa8608 to bbb66d3 Compare July 14, 2026 09:48
@orestisfl orestisfl marked this pull request as ready for review July 14, 2026 10:14
@orestisfl orestisfl requested a review from a team as a code owner July 14, 2026 10:14
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

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

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

Looks good!

@orestisfl orestisfl enabled auto-merge (squash) July 15, 2026 09:40
@mergify

mergify Bot commented Jul 15, 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

@github-actions

Copy link
Copy Markdown
Contributor

TL;DR

The Buildkite job failed with one unidentified Go integration-test failure after 2,884 tests. The available job log does not include the failing test assertion or stack trace; it only records the final teardown of TestUDPInputOTelE2E and then Error: failed to execute go: exit status 1. This is not attributable to the PR's filestream changes from the available evidence.

Remediation

  • Re-run the failed job, or isolate TestUDPInputOTelE2E in x-pack/filebeat/tests/integration to determine whether this is transient.
  • Retrieve and inspect the uploaded TEST-go-integration.out.json artifact and the TestUDPInputOTelE2E*/stderr/stdout files; the console log omitted the actual failure details.
  • If reproducible, use the assertion and Elasticsearch query error from that artifact to fix the UDP OTel E2E setup/query path before changing this PR.
Investigation details

Root Cause

The root cause is inconclusive because the provided Buildkite log contains no failing test name, assertion, or stack trace. The only identifiable test artifact is TestUDPInputOTelE2E509501367. Its implementation is in x-pack/filebeat/tests/integration/otel_tcp_udp_test.go:50-64; the polling assertions that can fail are at x-pack/filebeat/tests/integration/otel_tcp_udp_test.go:192-205. The PR diff is limited to filebeat/input/filestream/fswatch.go and filebeat/input/filestream/fswatch_test.go, which is unrelated to this UDP OTel integration path.

Evidence

  • Build: https://buildkite.com/elastic/beats/builds/49499
  • Job/step: x-pack/filebeat: Go Integration Tests (https://buildkite.com/elastic/beats-xpack-filebeat/builds/34231)
  • Key log excerpt: DONE 2884 tests, 11 skipped, 1 failure in 1016.340s followed by Error: failed to execute go: exit status 1. The preceding output shows normal shutdown for udp-input-e2e and error":"context canceled", which is teardown output rather than the failing assertion.
  • The log also confirms upload of TEST-go-integration.out.json and TestUDPInputOTelE2E509501367/{stdout,stderr}, but their contents were not included in the prefetched data.

Verification

  • Not run locally: the failure details required to select and reproduce the exact failing assertion were not present in the prefetched artifacts.

Follow-up

Once the test artifact is available, check whether the failure is the 3-minute document polling in runSocketInputOTelE2E; if so, capture the exact Elasticsearch/query or input-delivery error before proposing a code change.


What is this? | From workflow: PR Buildkite Detective

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

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