Skip to content

[9.5](backport #51801) filestream: fix duplicate harvesters on growing fingerprint migration#51826

Merged
orestisfl merged 1 commit into
9.5from
mergify/bp/9.5/pr-51801
Jul 9, 2026
Merged

[9.5](backport #51801) filestream: fix duplicate harvesters on growing fingerprint migration#51826
orestisfl merged 1 commit into
9.5from
mergify/bp/9.5/pr-51801

Conversation

@mergify

@mergify mergify Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Proposed commit message

With file_identity.fingerprint.growing enabled, the registry key
migration that follows a file's fingerprint growth raced with
asynchronously starting harvesters. Start reserves the source id
synchronously, but the harvester goroutine only registers and locks its
resource when it gets to run. A migration landing in that window ignored
the reserved-but-unregistered harvester, so the follow-up Start of the
new key spawned a second harvester for the same file, while the pending
one registered under the stale key, locked a fresh offset-0 resource,
and re-read the whole file. Every line was ingested once per extra
harvester (2x-3x observed) and the migrated-away keys were resurrected
in the registry with cursor data.

The reader bookkeeping now stores a handle from reservation on and the
whole identity chain moves atomically:

- reserve() returns a *reader handle; the goroutine upgrades it via
  register(), which follows any re-keying that happened in the meantime
  and fails if the reservation was removed, so a Stop that arrives
  before the harvester starts now sticks.
- HarvesterGroup.Migrate re-keys the registry entry (UpdateKey) and any
  registration in one critical section, and derives the new key from the
  Source so it always matches what a follow-up Start reserves.
- Harvesters read their (key, resource) pair atomically; because
  UpdateKey re-keys resources in place, the pair stays live across
  migrations and a stale key can no longer mint a fresh resource.
- UpdateKey rolls back its in-memory swap when persisting fails, so a
  failed migration leaves a consistent old-keyed world; the prospector
  keeps the file flowing under its old identity and retries on a later
  scan, pruning index entries whose registry key is gone (ErrKeyGone).
- Stop is synchronous so a queued Stop can never cancel a newer Start's
  reservation, and stopping is no longer delayed behind the
  harvester_limit semaphore.

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. (skip-changelog: the growing fingerprint feature is unreleased)

Disruptive User Impact

None.

How to test this PR locally

Unit and integration tests:

go test -race ./filebeat/input/filestream/...
go test -tags integration ./filebeat/input/filestream/...

Reproduce with harvester_limit which keeps starting harvesters queued on the task group semaphore.

filebeat.inputs:
  - type: filestream
    id: repro-growing
    paths: ["/tmp/gfp/logs/*.log"]
    prospector.scanner.check_interval: 100ms
    prospector.scanner.fingerprint.enabled: true
    prospector.scanner.fingerprint.offset: 0
    prospector.scanner.fingerprint.length: 1024
    file_identity.fingerprint.growing: true
    harvester_limit: 10
    close.on_state_change.inactive: 500ms
output.file:
  path: /tmp/gfp/out
  filename: events

Create ~30 files with ~600 bytes (below the 1024-byte fingerprint threshold), start filebeat, then append lines in two waves so each file first grows below the threshold and then crosses it. Group the output docs by (log.file.path, log.offset): before this fix files that were queued behind the limit get every line 2-3 times (and filebeat.harvester.started exceeds the file count); after it every pair appears exactly once.


This is an automatic backport of pull request #51801 done by [Mergify](https://mergify.com).

…#51801)

With file_identity.fingerprint.growing enabled, the registry key
migration that follows a file's fingerprint growth raced with
asynchronously starting harvesters. Start reserves the source id
synchronously, but the harvester goroutine only registers and locks its
resource when it gets to run. A migration landing in that window ignored
the reserved-but-unregistered harvester, so the follow-up Start of the
new key spawned a second harvester for the same file, while the pending
one registered under the stale key, locked a fresh offset-0 resource,
and re-read the whole file. Every line was ingested once per extra
harvester (2x-3x observed) and the migrated-away keys were resurrected
in the registry with cursor data.

The reader bookkeeping now stores a handle from reservation on and the
whole identity chain moves atomically:

- reserve() returns a *reader handle; the goroutine upgrades it via
  register(), which follows any re-keying that happened in the meantime
  and fails if the reservation was removed, so a Stop that arrives
  before the harvester starts now sticks.
- HarvesterGroup.Migrate re-keys the registry entry (UpdateKey) and any
  registration in one critical section, and derives the new key from the
  Source so it always matches what a follow-up Start reserves.
- Harvesters read their (key, resource) pair atomically; because
  UpdateKey re-keys resources in place, the pair stays live across
  migrations and a stale key can no longer mint a fresh resource.
- UpdateKey rolls back its in-memory swap when persisting fails, so a
  failed migration leaves a consistent old-keyed world; the prospector
  keeps the file flowing under its old identity and retries on a later
  scan, pruning index entries whose registry key is gone (ErrKeyGone).
- Stop is synchronous so a queued Stop can never cancel a newer Start's
  reservation, and stopping is no longer delayed behind the
  harvester_limit semaphore.

(cherry picked from commit ba9c83b)
@mergify mergify Bot added the backport label Jul 9, 2026
@mergify mergify Bot requested a review from a team as a code owner July 9, 2026 06:33
@mergify mergify Bot requested review from andrzej-stencel and khushijain21 and removed request for a team July 9, 2026 06:33
@botelastic botelastic Bot added the needs_team Indicates that the issue/PR needs a Team:* label label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 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.

@github-actions github-actions Bot added bug Team:Elastic-Agent-Data-Plane Label for the Agent Data Plane team skip-changelog labels Jul 9, 2026
@botelastic botelastic Bot removed the needs_team Indicates that the issue/PR needs a Team:* label label Jul 9, 2026
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

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

@orestisfl orestisfl enabled auto-merge (squash) July 9, 2026 06:38
@orestisfl orestisfl merged commit bd967bc into 9.5 Jul 9, 2026
53 checks passed
@orestisfl orestisfl deleted the mergify/bp/9.5/pr-51801 branch July 9, 2026 08:26
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.

1 participant