Repository navigation
Prepare evaluator-gated dependent flag capabilities #13167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.datadog.openfeature.internal.evaluation; | ||
|
|
||
| import static java.util.Collections.emptySet; | ||
|
|
||
| import java.util.Set; | ||
|
|
||
| /** Capabilities supported by both local evaluation and exposure reporting. */ | ||
| public final class EvaluatorCapabilities { | ||
| public static final String DEPENDENT_FLAG_VARIANT_KEY_V1 = "dependent-flag-variant-key-v1"; | ||
|
|
||
| private EvaluatorCapabilities() {} | ||
|
|
||
| /** Keep empty until prerequisite evaluation, errors, and exposures are integration-tested. */ | ||
| public static Set<String> supported() { | ||
| return emptySet(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working with Aaron Silverman: [P1] Coordination note with #13166: nothing turns this report on, and #13166 sends the header by itself.
Suggested order: land this PR first. Then #13166 deletes its own header and constants, and this method returns |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.datadog.openfeature.internal.evaluation; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class EvaluatorCapabilitiesTest { | ||
| @Test | ||
| void doesNotAdvertiseUnsupportedDependentFlags() { | ||
| assertTrue(EvaluatorCapabilities.supported().isEmpty()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,18 @@ | ||
| package com.datadog.featureflag; | ||
|
|
||
| import static datadog.remoteconfig.Capabilities.CAPABILITY_FFE_FLAG_CONFIGURATION_RULES; | ||
| import static datadog.remoteconfig.Capabilities.CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1; | ||
| import static datadog.remoteconfig.Product.FFE_FLAGS; | ||
| import static java.util.Collections.emptySet; | ||
|
|
||
| import datadog.remoteconfig.ConfigurationChangesTypedListener; | ||
| import datadog.remoteconfig.ConfigurationPoller; | ||
| import datadog.remoteconfig.PollingRateHinter; | ||
| import java.util.IdentityHashMap; | ||
| import java.util.Iterator; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.concurrent.CopyOnWriteArrayList; | ||
| import java.util.function.Consumer; | ||
| import javax.annotation.Nullable; | ||
|
|
@@ -32,6 +37,8 @@ public class RemoteConfigServiceImpl implements ConfigurationChangesTypedListene | |
| private boolean unvalidated; | ||
|
|
||
| private boolean started; | ||
| private final Map<Consumer<byte[]>, Boolean> dependentFlagSubscribers = new IdentityHashMap<>(); | ||
| private boolean dependentFlagsAdvertised; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working with Aaron Silverman: [P2] Possible causes of the red
|
||
|
|
||
| public RemoteConfigServiceImpl(final ConfigurationPoller configurationPoller) { | ||
| this.configurationPoller = configurationPoller; | ||
|
|
@@ -64,11 +71,25 @@ private void listen() { | |
| * @return the handle to close to unsubscribe. | ||
| */ | ||
| public AutoCloseable subscribe(final Consumer<byte[]> subscriber) { | ||
| return subscribe(subscriber, emptySet()); | ||
| } | ||
|
|
||
| /** Subscribes with the evaluator capabilities reported by this SDK instance. */ | ||
| public AutoCloseable subscribe( | ||
| final Consumer<byte[]> subscriber, final Set<String> capabilities) { | ||
| start(); | ||
| // A distinct forwarding listener keeps repeated registrations independently removable. | ||
| final Consumer<byte[]> registered = subscriber::accept; | ||
| boolean requestAgain = false; | ||
| // Keep registration and replay atomic with notification snapshots. | ||
| synchronized (this.subscribers) { | ||
| this.subscribers.add(subscriber); | ||
| this.subscribers.add(registered); | ||
| this.dependentFlagSubscribers.put( | ||
| registered, capabilities.contains("dependent-flag-variant-key-v1")); | ||
| if (this.dependentFlagsAdvertised | ||
| && !capabilities.contains("dependent-flag-variant-key-v1")) { | ||
| this.current = null; | ||
| } | ||
| final byte[] document = this.current; | ||
| if (document != null) { | ||
| try { | ||
|
|
@@ -81,10 +102,39 @@ public AutoCloseable subscribe(final Consumer<byte[]> subscriber) { | |
| this.unvalidated = false; | ||
| } | ||
| } | ||
| updateDependentFlagCapability(); | ||
| if (requestAgain) { | ||
| requestAgain(); | ||
| } | ||
| return () -> this.subscribers.remove(subscriber); | ||
| return () -> { | ||
| synchronized (this.subscribers) { | ||
| this.subscribers.remove(registered); | ||
| this.dependentFlagSubscribers.remove(registered); | ||
| } | ||
| updateDependentFlagCapability(); | ||
| }; | ||
| } | ||
|
|
||
| private synchronized void updateDependentFlagCapability() { | ||
| final boolean supported; | ||
| synchronized (this.subscribers) { | ||
| supported = | ||
| this.started | ||
| && !this.dependentFlagSubscribers.isEmpty() | ||
| && !this.dependentFlagSubscribers.containsValue(false); | ||
| if (supported == this.dependentFlagsAdvertised) { | ||
| return; | ||
| } | ||
| this.dependentFlagsAdvertised = supported; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Comment from Claude working with Aaron Silverman: Heads up: This becomes a bug when When the advertised state changes, Scenario, assuming the Remote Config payload does not change when bit 50 is added (for example, an org without dependent flags), so the poller does not call
Fix: after the capability bit changes, call I found this by reading the code; I did not run it. |
||
| this.current = null; | ||
| } | ||
| // The poller invokes callbacks under its lock; never acquire it with subscribers locked. | ||
| if (supported) { | ||
| this.configurationPoller.addCapabilities(CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1); | ||
| } else { | ||
| this.configurationPoller.removeCapabilities( | ||
| CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -114,7 +164,11 @@ public synchronized void close() { | |
| this.configurationPoller.removeCapabilities(CAPABILITY_FFE_FLAG_CONFIGURATION_RULES); | ||
| this.configurationPoller.removeListeners(FFE_FLAGS); | ||
| this.configurationPoller.stop(); | ||
| this.subscribers.clear(); | ||
| synchronized (this.subscribers) { | ||
| this.subscribers.clear(); | ||
| this.dependentFlagSubscribers.clear(); | ||
| } | ||
| updateDependentFlagCapability(); | ||
| this.current = null; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 Comment from Claude working with Aaron Silverman:
Heads up: when the evaluator report is turned on, this gate sends
X-DD-FEATURE-FLAGS-CAPABILITIESon direct UFC requests (the report is empty today). The corresponding capability-gated code in DataDog/dd-trace-go#5800 and DataDog/dd-trace-js#11038 now uses the query parametercap_dependent-flags-variant-key=v1on direct UFC requests instead of this header (their capability reports are currently empty; DataDog/dd-trace-js#11083 states the reason: keep the capabilities header for client-to-edge negotiation, and negotiate the direct UFC response shape through the cache-keyed URL). If that is the agreed protocol, please switch this path to the same query parameter, so all SDKs negotiate the same way.