Skip to content

Harden Agentless Feature Flags EVP delivery - #12477

Open
leoromanovsky wants to merge 3 commits into
masterfrom
leoromanovsky/fflsdk-187-java-evp-fallback
Open

leoromanovsky wants to merge 3 commits into
masterfrom
leoromanovsky/fflsdk-187-java-evp-fallback

Conversation

@leoromanovsky

@leoromanovsky leoromanovsky commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Java already delivers agentless exposures (#12195) and flag evaluations (#12204) directly to EVP. This follow-up closes reliability gaps found while checking the cross-SDK contract: unsafe replay, inconsistent writer routes, lost Agent URL prefixes and permanent disablement when no route exists at startup. Tracks FFLSDK-187.

Changes

  • Share Agentless route state between writers, preserve URL prefixes, and require both identity-forwarding capabilities before selecting local EVP.
  • Keep direct routing sticky; recover only from unavailable state with bounded probes.
  • Disable underlying retries and redirects; replay only after 404/405 or a proven pre-connect failure. Keep credentials route-specific and Remote Config fixed to Agent EVP v2.
  • Bound exposure shutdown flushing, including trace-disabled operation, with lifecycle and concurrency coverage.

Decisions

  • SDK origin/version emission is isolated in stacked identity PR Send Java SDK identity with Feature Flagging EVP events #12575. This bottom PR alone is not completion of the identity contract.
  • Bounded shutdown remains explicit additional lifecycle scope here; it is independent of identity attribution.
  • No new product surface or system-test changes. Existing delivery is hardened rather than reimplemented from scratch.

Validation

  • Existing default-branch base: 3110972ccf744f304a7cf59973e240f9ccd1a071.
  • Exact reduced candidate: 1b5766b56c67bb7151a084abb752e62e5bb4b332.
  • ./gradlew :communication:spotlessApply :products:feature-flagging:feature-flagging-lib:spotlessApply :communication:test :products:feature-flagging:feature-flagging-lib:test --console=plain --max-workers=4: PASS; communication 234/234, feature-flagging-lib 279/279.
  • Capability regression proves missing or partial forwarding support is rejected even without identity-header configuration. Credential and no-replay tests remain below the identity split.
  • git diff --check: PASS. New scope-reduction commit is GitHub verified: verified=true, reason=valid.
  • No refreshed whole-agent lifecycle, CI, system-tests, dogfooding or backend-intake evidence for this candidate. Earlier results belong to e20a9d77c333ddb0c0444835f23fafba70952373 and must not be carried forward as current proof.
  • Fresh CI remains separate from local validation. This PR remains a draft.

Add capability-gated local discovery, safe sticky direct fallback, shared route state, send-once semantics, and bounded lifecycle handling for Java Feature Flags telemetry.

Environment: Datadog workspace
@leoromanovsky leoromanovsky added type: feature Enhancements and improvements tag: ai generated Largely based on code generated by an AI or LLM comp: openfeature OpenFeature labels Sep 12, 2026
@linear-code

linear-code Bot commented Sep 12, 2026

Copy link
Copy Markdown

FFLSDK-187

@datadog-official

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.00 s 13.98 s [-0.6%; +0.8%] (no difference)
startup:insecure-bank:tracing:Agent 13.00 s 13.01 s [-0.7%; +0.5%] (no difference)
startup:petclinic:appsec:Agent 16.96 s 16.76 s [+0.3%; +2.2%] (maybe worse)
startup:petclinic:iast:Agent 16.95 s 16.97 s [-0.9%; +0.6%] (no difference)
startup:petclinic:profiling:Agent 16.70 s 16.75 s [-1.4%; +0.7%] (no difference)
startup:petclinic:sca:Agent 16.87 s 16.13 s [-0.0%; +9.2%] (no difference)
startup:petclinic:tracing:Agent 16.05 s 16.31 s [-2.5%; -0.6%] (maybe better)

Commit: 1b5766b5 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

Move identity attribution to a follow-up branch while keeping forwarding-capability requirements explicit and independent of emitted headers. Preserve credential isolation, no-replay coverage, and existing lifecycle hardening.

Validation: communication tests 234/234 and feature-flagging-lib tests 279/279 pass; scoped Spotless formatting passes.

Environment: Datadog workspace
@leoromanovsky
leoromanovsky marked this pull request as ready for review September 21, 2026 14:42
@leoromanovsky
leoromanovsky requested review from a team as code owners September 21, 2026 14:42
@leoromanovsky
leoromanovsky requested review from bric3, btthomas, pavlokhrebto and vandonr and removed request for a team September 21, 2026 14:42
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-21T14:47:50.191853Z 1b5766b Draft marked ready
🔒 Security Review ✅ Completed 2026-09-21T14:51:15.140290Z 1b5766b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@datadog-official datadog-official Bot 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.

⚠️ Autotest was unable to complete this review. View session

Please try again by commenting @autotest review.

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

🤖 Generated with Claude Code

if (selectedRoute == FeatureFlagRouteSelector.Route.LOCAL) {
BackendApi selectedProxyApi = proxyApi;
if (selectedProxyApi == null) {
selectedProxyApi = discoverProxyApi();

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.

When the shared route is LOCAL but this writer's own proxyApi is unavailable, every post() forces a fresh, unthrottled Agent /info discovery instead of being rate-limited like the UNAVAILABLE-route recovery path. FeatureFlagRouteSelector is shared process-wide across the exposure and flag-evaluation writers, so if one writer transitions the shared route to LOCAL but this writer's own proxyApi stays null (e.g. createProxyApi(false) never satisfied supportsEvpProxyHeaders), selectApi() calls discoverProxyApi() -> createProxyApi(true) -> DDAgentFeaturesDiscovery.discover() (a real HTTP /info round-trip) on every single post(), with no cooldown, unlike the throttled Route.UNAVAILABLE path gated by routeSelector.tryBeginLocalRecovery().

🤖 Generated with Claude Code

final BackendApi directApi = getOrCreateDirectApi();
if (directApi == null) {
final BackendApi fallbackApi = getOrCreateDirectApi();
routeSelector.localFailure(fallbackApi != null);

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.

Any IOException on the local proxy route (including ambiguous/transient ones like HTTP 429/500 or a timeout) permanently flips the shared route away from LOCAL, and FeatureFlagRouteSelector has no transition back from DIRECT to LOCAL. post() calls routeSelector.localFailure(fallbackApi != null) unconditionally whenever the local call throws IOException, regardless of isSafeToReplayDirectly(exception). A single transient 500 from the local Agent's EVP proxy switches the shared route to DIRECT for the remainder of the process — both exposures and flag-evaluation writers bypass the local Agent from then on even after the Agent recovers, since Route.DIRECT is terminal. Note: this is explicitly test-covered (keepsDirectRouteStickyAfterLocalFailure), so it reads as intentional design rather than an oversight — flagging it as a reliability trade-off worth an explicit maintainer sign-off.

🤖 Generated with Claude Code

&& routeSelector.tryBeginLocalRecovery()) {
final BackendApi recoveredProxyApi = discoverProxyApi();
if (recoveredProxyApi != null) {
proxyApi = recoveredProxyApi;

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.

Unsynchronized read-then-write of the volatile proxyApi field lets concurrent callers race to discover/store the local proxy client. Two threads calling post() concurrently while proxyApi is null and the shared route is UNAVAILABLE-recovering or LOCAL will both observe proxyApi == null, both invoke discoverProxyApi() (unlike getOrCreateDirectApi(), which is synchronized and guarded by directApiCreationAttempted), each creating a redundant Agent-discovery client, with the later write winning non-deterministically.

🤖 Generated with Claude Code

}

@Override
public boolean isApplicable(Set<TargetSystem> enabledSystems) {

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.

isApplicable() is hard-coded to always return true, so this bootstrap instrumentation of java.lang.Shutdown now applies even when no product (tracing, feature flagging, or otherwise) is enabled. Previously this extended InstrumenterModule.Tracing, gated on TargetSystem.TRACING. Since there's no TargetSystem entry for Feature Flagging, every JVM the agent attaches to now gets java.lang.Shutdown instrumented with ByteBuddy advice even when dd.trace.enabled=false and feature flagging is never used — a blanket "always applicable" special case on a shared, sensitive ForBootstrap instrumentation rather than a general enabled-system check.

🤖 Generated with Claude Code

this.serializerThread.join(shutdownTimeoutMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

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.

In agentless mode, the writer now always starts and keeps its serializer thread alive forever (waking every 100ms) even when no delivery route (local proxy or direct credentials) will ever become available. Pre-PR, FeatureFlagBackendApiFactory.create() returned null when neither route was available, and the writer thread exited immediately. Post-PR, create() always wraps the routes in an AgentlessFeatureFlagBackendApi, so a customer with agentless feature flags enabled but no API key and no compatible local Agent gets a permanently-idle thread polling queue.poll(100ms) for the life of the JVM instead of the thread terminating.

🤖 Generated with Claude Code

return discoveryState.telemetryProxyEndpoint != null;
}

private static HttpUrl appendPath(final HttpUrl baseUrl, final String path) {

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.

appendPath(HttpUrl, String) is duplicated verbatim in both this file and BackendApiFactory.java (line ~244). Both classes independently strip leading slashes and call addPathSegments identically. A future fix to path-joining semantics (e.g. handling encoded slashes, empty segments, or trailing-slash edge cases) applied to one copy but not the other will silently reintroduce the agent-base-path bug this PR fixes only where the fix is remembered. Consider extracting to a shared helper.

🤖 Generated with Claude Code

This branch has not been deployed

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

Labels

comp: openfeature OpenFeature tag: ai generated Largely based on code generated by an AI or LLM type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants