[9.5](backport #7662) feat: batch enrollment FindAgent searches with pre-refresh dedup - #7664
Merged
Conversation
* feat: batch enrollment FindAgent searches with pre-refresh dedup via kQueueEnrollSearch Adds a dedicated kQueueEnrollSearch bulker queue that prevents duplicate agent documents caused by concurrent enrollment retries with the same enrollment_id. The queue: - Flushes independently from other queues (default: every 1s or 50 items) - Calls POST .fleet-agents/_refresh before executing the msearch batch, ensuring retries see agent documents written by earlier requests - De-dupes concurrent requests with the same enrollment_id: the first (canonical) request gets the search result; duplicates receive ErrEnrollDuplicate (HTTP 429) so the agent retries after backoff Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add changelog fragment for kQueueEnrollSearch enrollment dedup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: rename EnrollBatcher → EnrollBulker in config and opts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: correct LIFO→FIFO order and refresh all unique indices in flushEnrollSearch Address two Copilot review findings in flushEnrollSearch: 1. The bulker queue is LIFO (head = newest item), so iterating queue.head directly made the *newest* request canonical, not the oldest. Collect all items into a slice and reverse it so the oldest request is canonical for each dedupeKey (FIFO semantics). 2. Only canonicals[0].refreshIndex was refreshed, silently skipping any additional indices present in the batch. Collect all unique refreshIndex values from every canonical and refresh them all. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: propagate canonical search error to duplicates in flushEnrollSearch When a canonical request's msearch returns an ES error, duplicates were receiving ErrEnrollDuplicate, which callers treat as a benign retry signal. This silently hid the real failure. Now: if deriveError() on a canonical response is non-nil, that error is forwarded to all duplicates sharing the same dedupeKey, so callers surface the actual operational failure rather than retrying indefinitely under the impression it was just a duplicate. ErrEnrollDuplicate is still sent when the canonical search succeeded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add integration test for enrollment search dedup (kQueueEnrollSearch) Fires numConcurrent goroutines all calling bulker.Search with the same dedupeKey against a test index. Sets enrollFlushThresholdCount above the concurrent count so all requests land in one batch and are flushed by the timer. Asserts exactly one canonical result and numConcurrent-1 ErrEnrollDuplicate responses. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address golangci-lint failures (nolintlint, goimports) - Add explanation to nolint:errcheck directive in flushEnrollSearch (nolintlint requires a reason comment) - Realign ServerBulk struct fields after adding the wider ServerBulkEnrollBulker type (goimports formatting) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: call EnrollBulker.InitDefaults() from ServerBulk.InitDefaults() ucfg calls each nested struct's InitDefaults() automatically when loading config from a file, so the EnrollBulker defaults (1s, 50) were correctly applied at runtime. However, tests that construct expected configs by calling InitDefaults() manually through the chain were getting zero values for EnrollBulker, causing TestConfig to fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: eliminate data race in flushEnrollSearch dispatch loop After sending on n.ch, the receiving goroutine (Search) immediately calls freeBlk(n) -> reset(), which zeroes all bulkT fields including dedupeKey. Reading n.dedupeKey after the channel send to look up duplicate waiters was therefore a data race detected by -race. Fix: capture n.dedupeKey into a local variable before the send, so the map lookup uses a stack-allocated copy that is not subject to concurrent modification. This matches the pattern already used in flushSearch, which saves n.next before its channel send with the comment "n is invalid immediately on channel send". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: rename enroll_bulker config key to enroll, simplify changelog - inputs[].server.bulk.enroll_bulker -> inputs[].server.bulk.enroll (per blakerouse feedback: _bulker suffix is redundant in this context) - Rewrite changelog to focus on the user-visible problem (ghost agents) rather than implementation details Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> (cherry picked from commit 3890bbb)
5 tasks
Contributor
Author
|
This pull request has not been merged yet. Could you please review and merge it @ycombinator? 🙏 |
swiatekm
approved these changes
Aug 24, 2026
Contributor
Author
|
Tick the box to add this pull request to the merge queue (same as
|
ycombinator
approved these changes
Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the problem this PR solves?
At 30k+ agent scale, enrollment retries can create duplicate (ghost) agent documents in
.fleet-agents. The root cause is ES near-real-time search latency: when an agent retries enrollment before the previous write is visible to search (default refresh interval: 1s on ES, 5s on Serverless), a concurrent retry also finds "no existing agent" and writes a second document with the sameenrollment_id.How does this PR solve the problem?
Adds a new
kQueueEnrollSearchbulker queue that batches enrollmentFindAgentsearches and, before executing them, fires a singlePOST .fleet-agents/_refreshto make all recent writes visible. Within each batch:refreshIndexvalues in the batch are refreshed so retries see the most recent writes.enrollment_id(passed viaWithDedupeKey) are grouped by FIFO order — the oldest (first) request is canonical and is included in the msearch; duplicates receive HTTP 429 (ErrEnrollDuplicate) and retry after backoff.ErrEnrollDuplicate, so operational failures are not silently hidden.The queue flushes when N requests accumulate (default: 50) or after M seconds (default: 1s), whichever comes first — capping refresh calls at one per fleet-server instance per flush window. Both thresholds are configurable via
inputs[].server.bulk.enroll.flush_intervalandinputs[].server.bulk.enroll.flush_threshold_cnt.How to test this PR locally
Run the integration test added in this PR:
For manual testing, set a low
flush_threshold_cnt(e.g. 2) in config and fire concurrent enrollment requests with the sameenrollment_id. Only one agent document should be created.Design Checklist
op_type: createwrite guarantees in ES ensure correctness across instances.)Checklist
./changelog/fragmentsusing the changelog toolRelated issues
This is an automatic backport of pull request feat: batch enrollment FindAgent searches with pre-refresh dedup #7662 done by Mergify.