Skip to content

[9.5](backport #7688) fix: ignore missing .fleet-agents index on enrollment _refresh path - #7690

Merged
ycombinator merged 1 commit into
9.5from
mergify/bp/9.5/pr-7688
Aug 27, 2026
Merged

[9.5](backport #7688) fix: ignore missing .fleet-agents index on enrollment _refresh path#7690
ycombinator merged 1 commit into
9.5from
mergify/bp/9.5/pr-7688

Conversation

@mergify

@mergify mergify Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What is the problem this PR solves?

Fleet Server PR #7662 introduced a pre-enrollment _refresh call on .fleet-agents to make prior writes visible before the deduplication msearch in kQueueEnrollSearch. On a fresh Serverless project the index does not yet exist, so _refresh returns HTTP 404. This causes failQueue to be called and every enrollment attempt to fail permanently — a deadlock where the index is never created because enrollment always fails before the first write can land.

Only enrollments that include enrollment_id are affected (they are routed through kQueueEnrollSearch). Horde always sets enrollment_id; Elastic Agent may also set it. Agents that omit enrollment_id bypass this path and can enroll successfully, but they do not unblock the deadlocked agents.

Discovered while investigating why Horde project drones never come online in Serverless staging (elastic/horde#569). ECH drones were unaffected because the Horde k8s smoke test deploys its own older fleet-server binary that predates #7662.

How does this PR solve the problem?

Two fixes:

1. Pass ignore_unavailable=true to the refresh request.
When .fleet-agents does not exist the refresh becomes a no-op (HTTP 200, zero shards) instead of a fatal 404. The first enrollment proceeds to the msearch, the agent document is written, and ES auto-creates .fleet-agents-7 with the correct alias and mappings via its system index descriptor. Subsequent retries with the same enrollment_id then deduplicate correctly.

2. Fix body-close ordering (also introduced in #7662).
refreshResp.Body.Close() was called before refreshResp.String() in the error path, causing every enrollment failure on this path to be reported as:

enroll search refresh failed: <error reading response body: http: read on closed response body>

instead of the actual ES error. Changed to defer refreshResp.Body.Close().

How to test this PR locally

  1. Stand up a fresh Serverless Elasticsearch project (no .fleet-agents index).
  2. Enroll an agent with enrollment_id set.
  3. Confirm enrollment succeeds and .fleet-agents-7 is created with the .fleet-agents alias.
  4. Confirm a second enrollment with the same enrollment_id returns ErrEnrollDuplicate.

Design Checklist

  • I have ensured my design is stateless and will work when multiple fleet-server instances are behind a load balancer.
  • I have or intend to scale test my changes, ensuring it will work reliably with 100K+ agents connected.
  • I have included fail safe mechanisms to limit the load on fleet-server: rate limiting, circuit breakers, caching, load shedding, etc.

Checklist

  • 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
  • I have added an entry in ./changelog/fragments using the changelog tool

Related issues

…th (#7688)

* fix: close enroll search refresh response body after reading error

When the pre-enrollment _refresh call returns a non-2xx response, the
body was closed before String() was called to format the error message,
producing "error reading response body: http: read on closed response
body" instead of the actual ES error. Defer the close so String() can
read the body first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: ignore missing .fleet-agents index on enrollment _refresh

On a fresh Serverless project .fleet-agents does not exist when the first
enrollment attempt arrives. The pre-enrollment _refresh call introduced in
#7662 returned HTTP 404, which caused failQueue to be called and every
enrollment to fail permanently — a deadlock where the index is never created
because enrollment always fails before the first write lands.

Pass ignore_unavailable=true to the refresh request so a missing index is
treated as a no-op (HTTP 200, zero shards). The first enrollment proceeds to
the msearch, writes the agent document (ES auto-creates .fleet-agents-7 with
the correct alias and mappings via the system index descriptor), and subsequent
retries with the same enrollment_id deduplicate correctly.

Also fix the body-close ordering introduced in #7662: Body.Close() was called
before String() in the error path, hiding the real ES error behind
"read on closed response body".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fixup: address review feedback — close refresh body immediately, add missing-index integration test, remove unneeded changelog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 9ab9134)
@mergify
mergify Bot requested a review from a team as a code owner August 26, 2026 16:33
@mergify mergify Bot added the backport label Aug 26, 2026
@mergify
mergify Bot requested review from swiatekm and ycombinator and removed request for a team August 26, 2026 16:33
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

TL;DR

This build failed in two places: the benchmark step still lacks first-failure context (same limitation as the prior detective note), and the new E2E failure is in long-running TestStandAloneRunningSuite cases that are timing/external-dependency sensitive (TestOpAMPWithUpstreamCollector, TestAgentGracefulForceUnenroll). Immediate action is to rerun once; if it repeats, capture full per-test failure output (not tail-only logs).

Remediation

  • Retry the failed Buildkite jobs once to confirm transience (Run go benchmark for PR branch, E2E Test).
  • For repeated E2E failures, prioritize stabilizing testing/e2e/stand_alone_test.go by reducing external build/network variance in TestOpAMPWithUpstreamCollector and widening/decoupling timing windows in TestAgentGracefulForceUnenroll.
  • For repeated benchmark failures, inspect the uploaded build/next.out artifact and use the first package-level failing line for root-cause attribution (tail log is insufficient).
Investigation details

Root Cause

The failing PR (#7690) changes only:

  • internal/pkg/bulk/opSearch.go
  • internal/pkg/bulk/enroll_search_integration_test.go

No E2E test code was modified in this PR, and the E2E failures occurred in tests with known high runtime variance:

  • testing/e2e/stand_alone_test.go:650 TestOpAMPWithUpstreamCollector performs git clone + make otelcontribcol under a 5-minute test context (stand_alone_test.go:653, :672-693), which is sensitive to CI CPU/network variability.
  • testing/e2e/stand_alone_test.go:750 TestAgentGracefulForceUnenroll runs a long orchestration flow with external download/process startup and multiple long Eventually windows (:751-753, :862-884, :902-940), also sensitive to timing jitter.

Related flaky tracking exists for this suite's OpAMP path:

  • #6590 [Flaky Test] TestStandAloneRunningSuite/TestOpAMP signal: killed

Evidence

  • Build: https://buildkite.com/elastic/fleet-server/builds/16472
  • Job/step: E2E Test
    • --- FAIL: TestStandAloneRunningSuite/TestAgentGracefulForceUnenroll (236.83s)
    • --- FAIL: TestStandAloneRunningSuite/TestOpAMPWithUpstreamCollector (300.32s)
    • FAIL github.com/elastic/fleet-server/testing/e2e 1544.970s
  • Job/step: Run go benchmark for PR branch
    • Error: running "go test -bench=Bench -tags=grpcnotrace -run=Bench -count=8 -benchmem ./..." failed with exit code 1
    • log only contains tail output and wrapper failure (no first benchmark/package failure line).

Verification

  • Not run in this environment: E2E depends on Docker-based integration flows and this runner does not support Docker-in-Docker.

Follow-up

If reruns keep failing, collect full failing sections (or artifact excerpts) for:

  1. the first failing benchmark package from build/next.out; and
  2. the first assertion/stacktrace inside each failed E2E test body.
    With those, I can provide a deterministic, line-level fix plan instead of flaky-triage guidance.

What is this? | From workflow: PR Buildkite Detective

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

@ycombinator
ycombinator enabled auto-merge (squash) August 27, 2026 12:22
@ycombinator
ycombinator merged commit 8c0c368 into 9.5 Aug 27, 2026
13 checks passed
@ycombinator
ycombinator deleted the mergify/bp/9.5/pr-7688 branch August 27, 2026 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant