Preserve Feature Flagging configuration source semantics#11992
Draft
leoromanovsky wants to merge 2 commits into
Draft
Conversation
This comment has been minimized.
This comment has been minimized.
Contributor
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
leoromanovsky
force-pushed
the
leo.romanovsky/ffl-2693-java-configuration-contract
branch
from
July 20, 2026 17:11
c5bae85 to
b32988c
Compare
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.
Motivation
Until now, Java customers could activate the Datadog OpenFeature provider only by setting
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true. That experimental input did more than enable a provider: the implementation it unlocked subscribed to the Agent'sFFE_FLAGSRemote Configuration product. Customers who set it therefore adopted Feature Flags with Remote Configuration's Agent connectivity, network path, and delivery assumptions.The new configuration model makes CDN-backed agentless delivery the default for new customers so they no longer need an experimental opt-in. We cannot reinterpret the existing legacy opt-in as agentless, however, because that would silently move an established customer to a different delivery system. During the migration window, explicit legacy
truemust remain on Remote Configuration, explicit legacyfalsemust remain disabled, and only an absent legacy value may receive the new agentless default.This also has a billing boundary. Requests to Remote Configuration and the CDN are billing surfaces, so "agentless is the default" cannot mean that installing or initializing the Java agent starts polling. The migration separates the concerns previously collapsed into the experimental switch:
DD_FEATURE_FLAGS_ENABLEDis the global kill switch,DD_FEATURE_FLAGS_CONFIGURATION_SOURCEchooses a delivery system when explicit, and application initialization or access to the Datadog OpenFeature provider activates default agentless polling. This PR implements that precedence on top of sealed #11892 without creating phantom traffic or breaking the existing RC cohort.flowchart TD Start["Agent starts"] --> Enabled{"DD_FEATURE_FLAGS_ENABLED"} Enabled -->|"false"| Disabled["Provider disabled<br/>No RC subscription<br/>No CDN polling"] Enabled -->|"true or unset"| Explicit{"Explicit configuration source?"} Explicit -->|"remote_config"| RC["Eager FFE_FLAGS subscription"] Explicit -->|"agentless"| Wait["Wait for application provider access"] Explicit -->|"unsupported offline or invalid"| Closed["Fail closed<br/>No network delivery"] Explicit -->|"absent"| Legacy{"Legacy experimental variable?"} Legacy -->|"true"| RC Legacy -->|"false"| Disabled Legacy -->|"absent"| Wait Wait --> Access["Application initializes or accesses provider"] Access --> CDN["Start CDN-backed polling"]Changes
Separate enablement from source selection.
FeatureFlaggingConfig.Resolutionnow carries an enabled state independently from the selected source. A disabled provider has a null source; enabled configurations select onlyagentlessorremote_config.Apply the compatibility precedence once. Global
falsewins first, an explicit supported source wins next, and the legacy variable is consulted only when the new source is absent. The result is fixed during agent bootstrap.Activate only the selected delivery path. Explicit and grandfathered Remote Configuration remain eager. Agentless remains lazy until application initialization or provider access. Invalid input, including reserved
offline, fails closed without RC or CDN traffic.Preserve migration observability. Legacy variable usage continues through the existing deprecation path so adoption can be measured during the migration window.
Decisions
The kill switch is absolute.
DD_FEATURE_FLAGS_ENABLED=falsedisables the provider and prevents both CDN polling and theFFE_FLAGSsubscription.truemerely permits source resolution.Existing customers stay on Remote Configuration. With no explicit new source, legacy
truepreserves RC and legacyfalsepreserves disabled behavior. Only customers with no legacy value receive the new default.Agentless is the default, not startup traffic. New customers default to agentless, but polling begins only after application initialization or provider access. An explicit
agentlessorremote_configselection still overrides the legacy value.Only two sources are supported today.
agentlessandremote_configare valid.offlineis reserved for future startup-provided UFC bytes and currently fails closed; it is not the disable switch.Validation
Cross-repository system-tests proof
The exact Java head
7647879607f741c5961a450932c6a323689580a8was validated against mergedsystem-testsmainat54649784b2d2807522ebeaabae49ffe70e921014, which contains DataDog/system-tests#7332.The parametric image was supplied all three locally built artifacts, including
dd-openfeaturerather than the Maven-released provider:dd-java-agent-1.65.0-SNAPSHOT.jar6f5a773fb29e740766869b4a9393bc9ab7bce333a352734f1b04975205f581c7dd-openfeature-1.65.0-SNAPSHOT.jarea7fd8f937be7b197119c4923f5d634e60c13fde318abb981c479c797992b3e2dd-trace-api-1.65.0-SNAPSHOT.jaree0fc95dcfdc98530d3ce83ebab699d0fbc87b8dcfc24bf8d72997b443218fcdAt runtime the image reported
java@1.65.0-SNAPSHOT+7647879607, tying the result back to this PR head.Result: 20 passed in 33.04s.
agentlessbeats legacytrue; explicitremote_configbeats legacyfalse. Only the selected delivery path becomes active.trueselects RC; legacyfalsedisables delivery. The same behavior is proven when the legacy value is present but empty or whitespace.truepermits lazy default agentless behavior. Stablefalsesuppresses default agentless, explicit RC, and grandfathered legacy RC.offlineboth fail closed with RC and CDN silent.max_in_flight == 1; polling requests do not overlap.For completeness, an exploratory run of the entire 29-case file produced 28 passed, 1 failed. The remaining case is
server-error-to-validcold recovery: the current Java parametric application hard-codes a 100 ms provider initialization timeout while that scenario intentionally returns 500 first and configures the retry at 1 second. The provider consequently reports the expected initialization timeout, then transitions toPROVIDER_READYafter the retry; the test cannot also require the original synchronous/ffe/startcall to succeed within 100 ms. The source-selection and billing contract above is fully green; the remaining result is a system-test harness timing mismatch rather than a failure to recover.No-Agent dogfooding proof
The same Java artifacts were staged into
ffe-dogfoodingatc2ca5be1ae59d7ef083ef731957881d23f95ff00. The run used the CDN JSON:API response shape (data.type=universal-flag-configuration, UFC underdata.attributes) and started onlymock-intakeandapp-javafor the activation boundary. Nodatadog-agentservice or container was present. The evaluator was added only after that boundary had passed.Using local Java agent: .../dd-java-agent-1.65.0-SNAPSHOT.jarandApp built with local dd-openfeature JAR.Feature Flagging system awaiting application provider activation. After three seconds, mockconfiguration_requests_totalremained0.POST /activate-providerchangedproviderActivatedtotrue; mock request count became1at/api/v2/feature-flagging/config/rules-based/server./healthreportedproviderReady=true,providerState=PROVIDER_READY, and aPROVIDER_READYevent.POST /evaluatereturned flagffe-dogfooding-string-flag, variantdogfood-treatment, allocationdogfood-allocation, and reasonSTATIC./evaluaterecorded Java HTTP 200/success with the same flag, treatment, allocation, andSTATICreason./api/latest-resultsreported JavaPROVIDER_READY; the Java result wassuccesswith variantdogfood-treatmentand reasonSTATIC.The mock's JSON:API envelope adaptation and isolated Compose file were validation-only. The dogfood checkout was restored to a clean state after the run, and its mock tests still pass.