Skip to content

test: harden port-bind race in EPP integration harness#16

Open
chethanuk wants to merge 16 commits into
mainfrom
fix/issue-1066-port-bind-race
Open

test: harden port-bind race in EPP integration harness#16
chethanuk wants to merge 16 commits into
mainfrom
fix/issue-1066-port-bind-race

Conversation

@chethanuk

@chethanuk chethanuk commented Jul 19, 2026

Copy link
Copy Markdown
Owner

User description

What type of PR is this?

/kind test

What this PR does / why we need it:

The EPP hermetic integration tests intermittently fail on CI with
Server failed to bind port 127.0.0.1:<port>. GetFreePort allocates a port by
listening on :0, immediately closes the listener, and returns the bare int. The
controller-runtime manager binds that port a moment later (gRPC ext-proc, metrics,
health), so the OS can hand the freed port to another process in the window between
the close and the later bind, and the bind fails with EADDRINUSE.

This hardens the test harness only: the port-alloc -> manager-start -> readiness
sequence in NewTestHarness is wrapped in a bounded retry that re-rolls all three
free ports and restarts the manager only on an address-in-use bind failure
(bounded to 5 attempts, 500ms apart), resetting Prometheus metrics between attempts
and failing loud once the budget is exhausted so a genuine bind bug still surfaces.
The manager goroutine now surfaces mgr.Start's error on a channel instead of
swallowing it. GetFreePort's signature is unchanged.

New table-driven tests cover the classifier (isAddrInUse), the retry control flow
(RetryOnAddrInUse: re-roll, budget exhaustion, non-retryable pass-through), and the
non-fatal readiness wait (WaitExtProcReady).

Which issue(s) this PR fixes:

Fixes llm-d#1066

Release note (write NONE if no user-facing change):

NONE

CodeAnt-AI Description

Stabilize the EPP integration harness when ports are briefly taken

What Changed

  • The EPP integration harness now retries startup when a chosen port is stolen before the server binds, instead of failing the test immediately
  • Each retry re-picks fresh ports, resets per-attempt metrics, and stops after a small fixed number of tries so real bind problems still surface
  • The harness now waits for the server to become reachable and reports early manager startup failures instead of hiding them
  • Added tests for detecting port-in-use failures, retry behavior, and readiness waiting

Impact

✅ Fewer flaky CI failures in EPP integration tests
✅ Fewer "address already in use" startup failures
✅ Clearer failure reporting when the server never binds

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • Bug Fixes
    • Improved ext-proc integration startup by reserving and reusing a pre-bound gRPC listener, and waiting for the gRPC connection to be genuinely ready.
    • Pod reconciliation/datastore logic now propagates endpoint registration/update failures instead of reporting success.
    • Flow-control/request metrics now cap fairness_id cardinality and prune fairness-specific series when flows are garbage-collected.
  • New Features
    • Added a benchmark smoke run (bench-smoke) and a full-path flow-control benchmark.
  • Tests
    • Added integration tests for reserved-port defense, pre-bound dialing, and ext-proc readiness success/failure; added new datastore/metrics regression tests.

@codeant-ai

codeant-ai Bot commented Jul 19, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 13cfe92 Jul 19, 2026 · 11:05 11:11

Updated in place by CodeAnt AI · last 5 reviews

@codeant-ai

codeant-ai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@chethanuk, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c6aed834-2c0f-481e-a6b4-1ccbcd524c2a

📥 Commits

Reviewing files that changed from the base of the PR and between 85937fa and d8570ce.

📒 Files selected for processing (9)
  • pkg/epp/framework/plugins/requestcontrol/dataproducer/tokenizer/README.md
  • pkg/epp/framework/plugins/requestcontrol/dataproducer/tokenizer/estimate.go
  • pkg/epp/framework/plugins/requestcontrol/dataproducer/tokenizer/estimate_live_test.go
  • pkg/epp/framework/plugins/requestcontrol/dataproducer/tokenizer/estimate_test.go
  • pkg/epp/framework/plugins/requestcontrol/dataproducer/tokenizer/mm_estimate.go
  • pkg/epp/framework/plugins/requestcontrol/dataproducer/tokenizer/tokenizer.go
  • pkg/epp/metadata/consts.go
  • pkg/epp/util/request/headers.go
  • pkg/epp/util/request/headers_test.go
📝 Walkthrough

Walkthrough

The PR propagates datastore registration failures, bounds and removes flow-control metric series, expands benchmark coverage, and improves EPP listener reservation, gRPC readiness, and manager cleanup. It also updates CI, documentation, and Go module versions.

Changes

Datastore registration error propagation

Layer / File(s) Summary
Registration errors and resync retries
pkg/epp/datastore/datastore.go
Datastore endpoint registration failures now return aggregated errors, retry concurrent upserts, and force failed pool resynchronization to retry.
Reconciliation error handling and regression tests
pkg/epp/controller/*, pkg/epp/datalayer/fake_factory.go, pkg/epp/datastore/datastore_test.go, pkg/epp/requestcontrol/*, test/utils/server.go
Pod reconciliation and test helpers handle datastore errors, with fake endpoint and concurrency regression coverage.

Metric cardinality and flow cleanup

Layer / File(s) Summary
Fairness label bounding and deletion
pkg/epp/metrics/cardinality.go, pkg/epp/metrics/metrics.go
Fairness IDs are bounded across request and flow-control metrics, and flow-specific series can be deleted.
Flow metric garbage collection
pkg/epp/flowcontrol/registry/*, pkg/epp/metrics/cardinality_test.go, release-notes.d/unreleased/2119.md
Flow garbage collection prunes deleted series while preserving shared overflow series, with corresponding tests and release notes.

Flow-control benchmark suite

Layer / File(s) Summary
Detector and full-path benchmark harness
pkg/epp/flowcontrol/benchmark/*
Benchmark detector bookkeeping, warmup, full-path components, telemetry, dispatch assertions, and package documentation are added or revised.
Benchmark smoke command and CI wiring
Makefile, .github/workflows/ci-pr-checks.yaml
A one-iteration benchmark smoke target is added and invoked in CI.

EPP listener reservation and readiness

Layer / File(s) Summary
Pre-bound ext-proc listener serving
cmd/epp/runner/test_runner.go, internal/runnable/grpc.go, pkg/epp/server/runserver.go
The runner accepts an optional listener and uses shared gRPC serving and graceful shutdown logic.
Connectivity readiness and validation
test/integration/util.go, test/integration/util_test.go, test/utils/network.go
Ext-proc setup waits for gRPC connectivity readiness and handles manager errors, cancellation, and timeouts; tests cover binding and readiness.
Harness startup and cleanup wiring
test/integration/epp/harness.go
The harness reserves the listener, wires its port and listener through startup, tracks manager shutdown, and orders cleanup.

Documentation and module updates

Layer / File(s) Summary
Architecture and metrics documentation
docs/architecture.md, docs/metrics.md
Architecture terminology, upstream references, and metric cardinality behavior are updated.
Go module version updates
go.mod
Direct and indirect module versions are bumped.

Estimated code review effort: 5 (Critical) | ~100 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NewTestHarness
  participant EPPManager
  participant ExtProcServer
  participant WaitExtProcReady
  participant ExtProcClient

  NewTestHarness->>EPPManager: start with reserved listener
  EPPManager->>ExtProcServer: serve ext-proc on listener
  NewTestHarness->>WaitExtProcReady: wait for connectivity Ready
  WaitExtProcReady->>ExtProcServer: establish gRPC connection
  ExtProcServer-->>WaitExtProcReady: readiness state
  WaitExtProcReady-->>ExtProcClient: readiness result
  NewTestHarness->>EPPManager: cancel and await shutdown
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: hardening the EPP integration harness against port-binding races.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
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
  • Commit unit tests in branch fix/issue-1066-port-bind-race

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.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Jul 19, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a retry mechanism for port binding in integration tests to resolve intermittent 'address already in use' failures. It adds helper functions such as isAddrInUse, RetryOnAddrInUse, and WaitExtProcReady, along with corresponding unit tests. The feedback points out an inconsistency in WaitExtProcReady where a nil error from mgrErr in the second select block is ignored, which could cause the harness to incorrectly report that the server is ready when the manager has actually terminated.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
test/integration/epp/harness.go (2)

223-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rationale duplicated across files — flagged for consolidation.

Restates the same TOCTOU/issue-#1066 explanation already documented on isAddrInUse in test/integration/util.go and on the portBindMaxAttempts/portBindRetryDelay constants above. See consolidated comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/integration/epp/harness.go` around lines 223 - 227, Remove the
duplicated rationale comment near the manager restart logic in the harness,
retaining only the implementation-relevant explanation already documented by
isAddrInUse, portBindMaxAttempts, and portBindRetryDelay.

Source: Coding guidelines


75-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rationale duplicated across files — flagged for consolidation.

Same issue-llm-d#1066/TOCTOU explanation as util.go's isAddrInUse doc and the attempt closure comment below. See consolidated comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/integration/epp/harness.go` around lines 75 - 80, The constants
portBindMaxAttempts and portBindRetryDelay duplicate the issue-#1066 rationale
documented elsewhere. Remove the repeated explanatory comment here and retain
only the concise constant declarations, leaving the existing retry behavior
unchanged.

Source: Coding guidelines

test/integration/util.go (1)

647-661: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rationale duplicated across files — flagged for consolidation.

This doc-comment restates the GetFreePort/bind TOCTOU rationale (issue #1066) that is also spelled out in test/integration/epp/harness.go (constants doc and the attempt closure comment). See consolidated comment below. As per coding guidelines, "State each fact once in its canonical location. Do not duplicate across struct docs, prose, tables, inline comments, and examples."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/integration/util.go` around lines 647 - 661, The comment above
isAddrInUse duplicates the GetFreePort/bind TOCTOU rationale documented in the
epp harness. Remove the duplicated rationale and retain only a concise
description of what isAddrInUse detects, while preserving the function’s
existing behavior and inline fallback comments.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/integration/epp/harness.go`:
- Around line 223-227: Remove the duplicated rationale comment near the manager
restart logic in the harness, retaining only the implementation-relevant
explanation already documented by isAddrInUse, portBindMaxAttempts, and
portBindRetryDelay.
- Around line 75-80: The constants portBindMaxAttempts and portBindRetryDelay
duplicate the issue-#1066 rationale documented elsewhere. Remove the repeated
explanatory comment here and retain only the concise constant declarations,
leaving the existing retry behavior unchanged.

In `@test/integration/util.go`:
- Around line 647-661: The comment above isAddrInUse duplicates the
GetFreePort/bind TOCTOU rationale documented in the epp harness. Remove the
duplicated rationale and retain only a concise description of what isAddrInUse
detects, while preserving the function’s existing behavior and inline fallback
comments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5fcd49c3-c5bd-418f-89aa-74190630ea33

📥 Commits

Reviewing files that changed from the base of the PR and between c82561b and 13cfe92.

📒 Files selected for processing (3)
  • test/integration/epp/harness.go
  • test/integration/util.go
  • test/integration/util_test.go

Repository owner deleted a comment from gemini-code-assist Bot Jul 19, 2026
Repository owner deleted a comment from codeant-ai Bot Jul 19, 2026
@chethanuk
chethanuk force-pushed the fix/issue-1066-port-bind-race branch from 21b251b to f87269b Compare July 21, 2026 14:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
test/integration/util.go (1)

655-655: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Typo: extPorcConnSetupPollIntervalextProcConnSetupPollInterval.

Minor misspelling ("Porc") in the constant name; harmless but hurts searchability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/integration/util.go` at line 655, Correct the misspelled constant
reference in the context timeout setup from extPorcConnSetupPollInterval to
extProcConnSetupPollInterval, using the existing correctly named constant and
leaving the surrounding waitCtx/cancel logic unchanged.
internal/runnable/grpc.go (1)

62-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant comments restate the same fact.

Lines 62-63 both say "shut down / terminate on context closed" — pick one. As per coding guidelines, comments should "state each fact once in its canonical location" and stay terse.

✏️ Trim to one line
-	// Shutdown on context closed.
-	// Terminate the server on context closed.
-	// Make sure the goroutine does not leak.
+	// Terminate the server on context closed; doneCh prevents the goroutine from leaking.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/runnable/grpc.go` around lines 62 - 64, Remove one of the duplicate
shutdown comments in the context-closed handling near the runnable gRPC server,
keeping a single terse comment that states the server terminates when the
context closes; preserve the separate goroutine-leak note if it conveys distinct
information.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/runnable/grpc.go`:
- Around line 62-64: Remove one of the duplicate shutdown comments in the
context-closed handling near the runnable gRPC server, keeping a single terse
comment that states the server terminates when the context closes; preserve the
separate goroutine-leak note if it conveys distinct information.

In `@test/integration/util.go`:
- Line 655: Correct the misspelled constant reference in the context timeout
setup from extPorcConnSetupPollInterval to extProcConnSetupPollInterval, using
the existing correctly named constant and leaving the surrounding waitCtx/cancel
logic unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1c8223be-6ec0-48b1-8962-4cc46916dd60

📥 Commits

Reviewing files that changed from the base of the PR and between 13cfe92 and f87269b.

📒 Files selected for processing (7)
  • cmd/epp/runner/test_runner.go
  • internal/runnable/grpc.go
  • pkg/epp/server/runserver.go
  • test/integration/epp/harness.go
  • test/integration/util.go
  • test/integration/util_test.go
  • test/utils/network.go

GetFreePort returns a vacated port that the manager binds a moment later,
so the OS can hand the port to another process in that window and the bind
fails intermittently with 'address already in use' (issue llm-d#1066).

Wrap the harness bring-up in a bounded retry that re-rolls the free ports
and restarts the manager only on an EADDRINUSE bind failure, resetting
metrics between attempts and failing loud once the budget is exhausted so
a genuine bind bug still surfaces. GetFreePort's signature is unchanged.

Signed-off-by: ChethanUK <chethanuk@outlook.com>
The per-attempt cancellable context reassigns the outer mgrCtx inside the
retry closure, which fatcontext flags as a nested context. It is intentional
(each attempt derives a fresh context from the stable parent and cancels the
prior one), so suppress it with a justified nolint. Verified with golangci-lint
on ./test/integration/... = 0 issues.

Signed-off-by: ChethanUK <chethanuk@outlook.com>
WaitExtProcReady's pre-dial select already reports an error when the
manager exits with a nil error, but the post-dial select fell through to
return ready on the same condition. A nil there means the manager returned
(context cancelled) and is shutting down, not that the ext-proc server is
ready, so mirror the pre-dial block and fail loud instead of handing back a
dying server.

Signed-off-by: ChethanUK <chethanuk@outlook.com>
The hermetic harness picked three random ports with GetFreePort, which binds,
closes, and returns the number, leaving a window for another process to take
the port before the server binds it. That is the "address already in use"
flake in issue llm-d#1066. Only the ext_proc port is ever used by a test.

Let the kernel assign the metrics and health ports instead. A bind on port 0
cannot collide, so two thirds of the race surface goes away without any
production change. The metrics server itself stays up, because the pprof and
plugin-state debug handlers register through it.

This also removes a self-collision: GetFreePort was called three times back to
back, each closing its listener before the next, so two calls could return the
same port. One call now remains.

Refs: llm-d#1066
Signed-off-by: ChethanUK <chethanuk@outlook.com>
GetFreePort selects a port by binding, closing, and returning the number; the
server binds it again a moment later. Between the close and the bind the OS can
hand that port to another process, which is the "address already in use" flake
in issue llm-d#1066.

Hand the ext_proc server a listener that is already bound instead. Holding the
listener open means the port is never released, so there is no window to lose.
Verified: with a listener held, draining the entire ephemeral port range never
reassigns that port, and a plain bind of it fails with EADDRINUSE.

ExtProcServerRunner gains an optional GrpcListener that takes precedence over
GrpcPort, following manager.Server's Listener field, which documents the same
pattern for the same reason. It defaults to nil, so production keeps binding
from the CLI-supplied port and options.go and the flags are untouched. The
health runnable also stays on the port path, which keeps the net.Listen branch
under test.

Refs: llm-d#1066
Signed-off-by: ChethanUK <chethanuk@outlook.com>
@chethanuk
chethanuk force-pushed the fix/issue-1066-port-bind-race branch from f87269b to 9cb8caa Compare July 21, 2026 14:30
A held listener cannot lose a port race, so RetryOnAddrInUse and isAddrInUse
have nothing left to retry. Removing them returns the harness to straight-line
startup: no per-attempt manager rebuild and no matching on the text of the
aggregated manager error.

The dial-based readiness check has to go with them, for a subtler reason. The
listener is bound before the server starts, so the kernel completes the
handshake out of the listen backlog and a dial succeeds while grpc.Serve has
not run yet. Waiting for the client connection to reach connectivity.Ready
requires the HTTP/2 handshake, which only completes once the server is
serving, and it still selects on the manager's exit so a start failure fails
fast instead of hanging.

Both claims are gated by tests rather than asserted:
TestReserveListenerDefendsPort holds a reservation, draws 2000 ephemeral
ports and shows none of them is the reserved one, and shows an explicit bind
of it gets EADDRINUSE; TestPreBoundListenerQueuesDials dials before Serve and
shows the request is still served.

Refs: llm-d#1066
Signed-off-by: ChethanUK <chethanuk@outlook.com>
@chethanuk
chethanuk force-pushed the fix/issue-1066-port-bind-race branch from 9cb8caa to 8c66909 Compare July 21, 2026 14:45
LukeAVanDrie and others added 8 commits July 21, 2026 10:03
…uite (llm-d#1638)

* test(flowcontrol): harden and reorganize the flow-control benchmark suite

Test-only changes to pkg/epp/flowcontrol/benchmark; no production code is touched.

- Re-scope the full-path benchmark (FullPathStress -> FullPath) to what it
  uniquely measures: dispatch throughput and allocation overhead through the real
  producer -> concurrency detector -> controller path. Drop the off-layer producer
  leak-counter assertion (covered by the inflightload producer's own unit tests),
  report d/s like the other benchmarks, and fix a b.Fatalf called from inside
  RunParallel (invalid off the benchmark goroutine).

- Fix the mock benchDetector so the PerformanceMatrix L sweep is meaningful: it
  accumulates in-flight up to the limit and reports saturated once exceeded (real
  W>L backpressure), while self-healing the empty-queue idle grants the processor's
  1ms dispatch ticker would otherwise leak (which previously latched and hung at
  L=1). Distinguish leaked idle grants from genuine load saturation via a release
  counter. Add detector unit tests covering saturation-at-limit and self-heal.

- Replace fixed bootstrap time.Sleep calls with a warmup request that blocks until
  dispatch -- deterministic readiness instead of arbitrary timing.

- Reorganize into doc.go (two-family overview) + helpers_test.go + benchmark_test.go,
  and dedup the shared registry/policy setup.

- Add a bench-smoke make target and CI step (go test -bench=. -benchtime=1x) so
  benchmark bodies execute on every PR; go test only compiles them otherwise,
  letting runtime breakage (panics, deadlocks) go undetected.

Signed-off-by: Luke Van Drie <lukevandrie@google.com>

* test(flowcontrol): correct FullPath backpressure comment

The inline comment claimed the FullPath benchmark sustains W>L queuing, but
worker count rounds down to <= L and the real detector's in-flight load is
released near-instantly, so dispatch is effectively free-flowing. Reword to
match the function godoc: FullPath prices dispatch + the detector's per-cycle
DynamicAttribute reads, not sustained backpressure.

Signed-off-by: Luke Van Drie <lukevandrie@google.com>

* test(flowcontrol): address review feedback on benchmark suite

- Use the llm-d Authors copyright header on the new files (doc.go,
  detector_test.go) per review.
- Fail fast if the PerformanceMatrix warmup request is not dispatched,
  matching the full-path harness, instead of silently timing a dead
  dispatch loop.
- Fail fast if a matrix coordinate dispatches zero requests, so a
  wedged dispatch path produces a red build instead of plausible
  zero rows.
- Drop the matrix harness TTL from 5m to 30s: nothing in a throughput
  benchmark legitimately waits minutes, and a tight TTL bounds a
  wedged coordinate to seconds of CI time instead of minutes per cell.
- Document the liveness bias of the benchDetector's stuck-read reclaim
  heuristic: true queueing still reports saturated while releases flow;
  a stalled release transiently over-admits rather than latching.

Signed-off-by: Luke Van Drie <lukevandrie@google.com>

---------

Signed-off-by: Luke Van Drie <lukevandrie@google.com>
… series (llm-d#2119)

* fix(metrics): bound fairness_id label cardinality and prune GC'd flow series

The fairness_id label on request and flow-control metrics is populated
from a client request header (or agent-identity attribute), so its
cardinality is not operator-bounded. Prometheus vectors never evict
label combinations: every distinct fairness ID ever observed permanently
allocated histogram series across both the deprecated and llm_d_epp
metric families, growing pod RSS and scrape payloads without bound.
Under flow-ID churn this was ~60% of remaining allocations in the
full-path flow-control benchmark.

Three changes, following the existing model-label precedent:

- Bound fairness_id through a boundedLabel limiter (cap 1000, overflow
  to "other") in every record function that takes it, exactly as model
  names are already bounded via boundModels.
- Bound model labels in RecordFlowControlRequestQueueDuration, which
  was missing the boundModels call its siblings have.
- Prune a flow's flow-control series (both families) when the registry
  garbage-collects the flow, so the vectors track live flows. Pruning
  runs after cleanupFlowResources and outside the registry lock;
  DeletePartialMatch scans whole vectors and must not run under it.

FullPath benchmark (registry/flow churn, M4 Pro, -benchtime=3s):
118k -> 213k d/s, 14.5KB -> 4.0KB per op, 264 -> 94 allocs per op.

Signed-off-by: Luke Van Drie <lukevandrie@google.com>

* test(metrics): cover fairness_id bounding paths; document label cardinality

- Verify the request-metric family bounds fairness_id like the flow
  control family, and that RecordFlowControlRequestQueueDuration bounds
  its model labels.
- Verify flow GC prunes the collected flow's metric series end to end.
- Document the client-derived label caps and GC pruning in
  docs/metrics.md.

Signed-off-by: Luke Van Drie <lukevandrie@google.com>

* fix(metrics): guard overflow series from flow GC pruning

A flow whose client-chosen fairness ID equals the overflow value must not
delete the shared series that aggregates every capped-out tenant, so
DeleteFlowControlFlowSeries now no-ops on the overflow value.

Also document the revive-during-GC gauge race as an accepted limitation and
clarify in docs/metrics.md that the model labels share one cap pool and that
caps apply over the process lifetime.

Signed-off-by: Luke Van Drie <lukevandrie@google.com>

---------

Signed-off-by: Luke Van Drie <lukevandrie@google.com>
…tly untracking pods (llm-d#2092)

* fix(datastore): surface dropped endpoint registrations and requeue pod reconciles

An upsert that overlaps an in-flight delete (or whose collector fails to
start) observes a nil endpoint from NewEndpoint with no surviving pods-map
entry. Previously this was silently swallowed: the pod stayed untracked
until the next pod event, which can leave the flow control dispatch cycle
with an empty candidate list and saturation pinned at 1.0.

upsertEndpoint now distinguishes the benign duplicate-start race (a
concurrent upsert already stored the entry) from a dropped registration,
which is returned as an error. PodReconciler propagates it so
controller-runtime requeues with backoff; podResyncAll propagates it
through PoolSet for the same effect on the pool reconciler; discovery
sources log it.

Related to llm-d#2060.

Signed-off-by: Luke Van Drie <lukevandrie@google.com>

* fix(datastore): retry resync after registration drop and apply metadata on upsert races

Close gaps in the dropped-registration handling:

- PoolSet stores the pool before resyncing, so a PoolSet retried after a
  resync failure compared the incoming pool against the already-stored
  identical pool and skipped the resync entirely. A needsResync flag now
  forces the retried PoolSet to resync until one succeeds.
- upsertEndpoint applies the caller's metadata through the update path
  when a concurrent upsert wins the registration race, instead of
  silently discarding it.
- The dropped-registration error is a sentinel (errRegistrationDropped)
  so callers and tests can match it with errors.Is.
- PodUpdateOrAddIfNotExist skips and logs at DEBUG when the pool is not
  synced instead of logging a misleading 'Pod already exists'; the
  added/exists logging lives in one place.
- Unchecked calls of the now error-returning PodUpdateOrAddIfNotExist
  are handled or explicitly discarded to satisfy errcheck.
- The bespoke test endpoint factories collapse into a configurable
  datalayer.FakeEndpointFactory.

Related to llm-d#2060.

Signed-off-by: Luke Van Drie <lukevandrie@google.com>

* chore: use llm-d Authors copyright in fake_factory.go

Signed-off-by: Luke Van Drie <lukevandrie@google.com>

* chore: fix requeueing typo flagged by CI

Signed-off-by: Luke Van Drie <lukevandrie@google.com>

---------

Signed-off-by: Luke Van Drie <lukevandrie@google.com>
…d#2128)

Signed-off-by: Luke Van Drie <lukevandrie@google.com>
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.81.1 to 1.82.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](grpc/grpc-go@v1.81.1...v1.82.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.82.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: llm-d-router-release-notes[bot] <287676111+llm-d-router-release-notes[bot]@users.noreply.github.com>
Co-authored-by: llm-d-router-release-notes[bot] <287676111+llm-d-router-release-notes[bot]@users.noreply.github.com>
* docs: align architecture.md terminology with the data layer

The architecture doc referred to a Scrapers component that no longer
exists; the metrics subsystem is the Data Layer (data sources plus
extractors). Rename the stale Scrapers references, describe the
Source -> Extract -> Attribute lifecycle, and link the canonical
upstream llm-d architecture docs the review comment pointed at.

Refs: llm-d#1307
Signed-off-by: ChethanUK <chethanuk@outlook.com>

* docs: rename Metric Scraping heading to include the data layer

The section body describes the data layer's Source -> Extract -> Attribute
lifecycle, but its heading and table-of-contents entry still read Metric
Scraping alone, leaving the last stale reference after the Scrapers -> Data
Layer rename. Update the heading and its TOC anchor to match.

Signed-off-by: ChethanUK <chethanuk@outlook.com>

* docs: data sources collect more than metrics

Review feedback on llm-d#2084: sources also poll served models via /v1/models
and include notification-based types, not only metric scrapers.

Signed-off-by: ChethanUK <chethanuk@outlook.com>

---------

Signed-off-by: ChethanUK <chethanuk@outlook.com>
@github-actions github-actions Bot added size/XXL and removed size/L labels Jul 22, 2026
* draft

Signed-off-by: Xiyue Yu <xiyue@google.com>

* added metadata

Signed-off-by: Xiyue Yu <xiyue@google.com>

* added video estimation support

Signed-off-by: Xiyue Yu <xiyue@google.com>

* rename variable

Signed-off-by: Xiyue Yu <xiyue@google.com>

* changed test

Signed-off-by: Xiyue Yu <xiyue@google.com>

* added gemma4 live test

Signed-off-by: Xiyue Yu <xiyue@google.com>

* fixed typo

Signed-off-by: Xiyue Yu <xiyue@google.com>

* adde logging info

Signed-off-by: Xiyue Yu <xiyue@google.com>

* changed config setting

Signed-off-by: Xiyue Yu <xiyue@google.com>

* renamed variable

Signed-off-by: Xiyue Yu <xiyue@google.com>

* updated readme

Signed-off-by: Xiyue Yu <xiyue@google.com>

* deleted debug logging

Signed-off-by: Xiyue Yu <xiyue@google.com>

* fixed linter

Signed-off-by: Xiyue Yu <xiyue@google.com>

* fixed linter

Signed-off-by: Xiyue Yu <xiyue@google.com>

---------

Signed-off-by: Xiyue Yu <xiyue@google.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
docs/architecture.md (1)

321-330: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid duplicating canonical upstream links.

This section repeats the Request Scheduler, Configuration, and Data Layer links already maintained at Lines 61, 82, and 227. Keep one canonical index or the inline references, rather than maintaining both.

As per coding guidelines: “State each fact once in its canonical location. Do not duplicate across struct docs, prose, tables, inline comments, and examples.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/architecture.md` around lines 321 - 330, Remove the duplicated Request
Scheduler, Configuration, and Data Layer entries from the “Canonical llm-d
architecture (upstream)” link list, preserving the existing canonical references
elsewhere in the document. Keep the remaining unique upstream links unchanged.

Source: Coding guidelines

pkg/epp/metrics/cardinality.go (1)

100-109: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove comments that only restate code.

  • pkg/epp/metrics/cardinality.go#L100-L109: retain only the client-controlled-label rationale; remove the boundFairnessID name/behavior restatement.
  • pkg/epp/metrics/metrics.go#L581-L584: remove or replace the comment with non-obvious rationale rather than paraphrasing the function name.

As per coding guidelines, “Comments are terse and only present when the WHY is non-obvious” and “State each fact once in its canonical location.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/epp/metrics/cardinality.go` around lines 100 - 109, Remove the comment
lines in pkg/epp/metrics/cardinality.go:100-109 that merely restate
boundFairnessID’s name or behavior, retaining only the non-obvious rationale
about client-controlled fairness ID cardinality. In
pkg/epp/metrics/metrics.go:581-584, remove the comment or replace it with
rationale that is not a paraphrase of the associated function name; no other
changes are needed.

Source: Coding guidelines

pkg/epp/datastore/datastore.go (2)

95-98: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Interface doc overstates the failure granularity.

The comment says a dropped registration means "the pod is then not tracked by the datastore," but podUpdateOrAddIfNotExist tracks per-endpoint: a multi-port pod can have some endpoints stored (added=true) while others are dropped and joined into the returned error. Worth tightening the wording so callers don't assume all-or-nothing semantics.

✏️ Suggested doc tweak
-	// PodUpdateOrAddIfNotExist stores or updates the endpoints for the given pod. It returns an
-	// error when an endpoint registration was dropped (see upsertEndpoint); the pod is then not
-	// tracked by the datastore and the caller must retry (e.g. by requeuing the reconcile).
+	// PodUpdateOrAddIfNotExist stores or updates the endpoints for the given pod. It returns an
+	// error when one or more of the pod's endpoint registrations were dropped (see upsertEndpoint);
+	// those endpoints are then not tracked by the datastore and the caller must retry (e.g. by
+	// requeuing the reconcile).
 	PodUpdateOrAddIfNotExist(ctx context.Context, pod *corev1.Pod) error
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/epp/datastore/datastore.go` around lines 95 - 98, Update the
PodUpdateOrAddIfNotExist interface documentation to describe per-endpoint
failure semantics rather than stating that the entire pod is untracked when an
endpoint registration is dropped. Clarify that some endpoints may remain stored
while dropped registrations are reported in the returned error, and callers
should retry the affected reconciliation as appropriate.

355-368: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

existingEpSet is populated but never read.

existingEpSet.Insert(...) is called for every endpoint of the pod, but the set is never consulted afterward — the "remove endpoints no longer active" loop below (lines 378-389) uses activePorts/pool.TargetPorts instead. This looks like leftover code from a refactor; either remove it or wire it into the cleanup logic if it was meant to replace the activePorts.Has(port) check.

🧹 Suggested cleanup
 	added := false
 	var errs []error
-	existingEpSet := sets.Set[types.NamespacedName]{}
 	for _, endpointMetadata := range pods {
-		existingEpSet.Insert(endpointMetadata.NamespacedName)
 		created, err := ds.upsertEndpoint(ctx, endpointMetadata)

Please confirm this wasn't intended to feed the cleanup loop below before removing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/epp/datastore/datastore.go` around lines 355 - 368, Remove the unused
existingEpSet declaration and its existingEpSet.Insert call from the endpoint
upsert loop; keep the cleanup logic based on activePorts and pool.TargetPorts
unchanged, confirming the set is not required elsewhere in the surrounding
function.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/epp/flowcontrol/registry/registry.go`:
- Around line 482-484: Update cleanupFlowResources and the flow
revival/metric-recording path so pruning is coordinated atomically with
recreated flows; do not delete a metric series for any key whose flow was
revived after keysToClean was collected. Ensure revived-flow increments remain
intact, using synchronization or generation-specific metric series as
appropriate while preserving cleanup for genuinely stale keys.

---

Nitpick comments:
In `@docs/architecture.md`:
- Around line 321-330: Remove the duplicated Request Scheduler, Configuration,
and Data Layer entries from the “Canonical llm-d architecture (upstream)” link
list, preserving the existing canonical references elsewhere in the document.
Keep the remaining unique upstream links unchanged.

In `@pkg/epp/datastore/datastore.go`:
- Around line 95-98: Update the PodUpdateOrAddIfNotExist interface documentation
to describe per-endpoint failure semantics rather than stating that the entire
pod is untracked when an endpoint registration is dropped. Clarify that some
endpoints may remain stored while dropped registrations are reported in the
returned error, and callers should retry the affected reconciliation as
appropriate.
- Around line 355-368: Remove the unused existingEpSet declaration and its
existingEpSet.Insert call from the endpoint upsert loop; keep the cleanup logic
based on activePorts and pool.TargetPorts unchanged, confirming the set is not
required elsewhere in the surrounding function.

In `@pkg/epp/metrics/cardinality.go`:
- Around line 100-109: Remove the comment lines in
pkg/epp/metrics/cardinality.go:100-109 that merely restate boundFairnessID’s
name or behavior, retaining only the non-obvious rationale about
client-controlled fairness ID cardinality. In
pkg/epp/metrics/metrics.go:581-584, remove the comment or replace it with
rationale that is not a paraphrase of the associated function name; no other
changes are needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86ddbc0f-638e-431c-ae14-30392f3bee5e

📥 Commits

Reviewing files that changed from the base of the PR and between f87269b and 85937fa.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (29)
  • .github/workflows/ci-pr-checks.yaml
  • Makefile
  • cmd/epp/runner/test_runner.go
  • docs/architecture.md
  • docs/metrics.md
  • go.mod
  • internal/runnable/grpc.go
  • pkg/epp/controller/pod_reconciler.go
  • pkg/epp/controller/pod_reconciler_test.go
  • pkg/epp/datalayer/fake_factory.go
  • pkg/epp/datastore/datastore.go
  • pkg/epp/datastore/datastore_test.go
  • pkg/epp/flowcontrol/benchmark/benchmark_test.go
  • pkg/epp/flowcontrol/benchmark/detector_test.go
  • pkg/epp/flowcontrol/benchmark/doc.go
  • pkg/epp/flowcontrol/benchmark/helpers_test.go
  • pkg/epp/flowcontrol/registry/registry.go
  • pkg/epp/flowcontrol/registry/registry_test.go
  • pkg/epp/metrics/cardinality.go
  • pkg/epp/metrics/cardinality_test.go
  • pkg/epp/metrics/metrics.go
  • pkg/epp/requestcontrol/director_test.go
  • pkg/epp/server/runserver.go
  • release-notes.d/unreleased/2119.md
  • test/integration/epp/harness.go
  • test/integration/util.go
  • test/integration/util_test.go
  • test/utils/network.go
  • test/utils/server.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • test/utils/network.go
  • cmd/epp/runner/test_runner.go
  • internal/runnable/grpc.go
  • pkg/epp/server/runserver.go
  • test/integration/util_test.go
  • test/integration/util.go
  • test/integration/epp/harness.go

Comment on lines +482 to +484
for _, key := range keysToClean {
metrics.DeleteFlowControlFlowSeries(key.ID, strconv.Itoa(key.Priority))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make metric pruning atomic with flow revival.

cleanupFlowResources skips a key when its flow has been recreated, but this loop still deletes that key’s live metric series. A revived flow can therefore lose an increment and later recreate a queue gauge with a negative value. Coordinate flow creation/metric recording with pruning, or use generation-specific series, rather than pruning every originally collected key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/epp/flowcontrol/registry/registry.go` around lines 482 - 484, Update
cleanupFlowResources and the flow revival/metric-recording path so pruning is
coordinated atomically with recreated flows; do not delete a metric series for
any key whose flow was revived after keysToClean was collected. Ensure
revived-flow increments remain intact, using synchronization or
generation-specific metric series as appropriate while preserving cleanup for
genuinely stale keys.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Flaky Test] hermetic integration tests fail with "Server failed to bind port"

3 participants