diff --git a/dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/main/java11/datadog/trace/instrumentation/openfeature/AgentConfigurationSource.java b/dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/main/java11/datadog/trace/instrumentation/openfeature/AgentConfigurationSource.java index 4ae21393a20..ae3d192cddb 100644 --- a/dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/main/java11/datadog/trace/instrumentation/openfeature/AgentConfigurationSource.java +++ b/dd-java-agent/instrumentation/datadog/openfeature/dd-openfeature-1.69/src/main/java11/datadog/trace/instrumentation/openfeature/AgentConfigurationSource.java @@ -2,6 +2,7 @@ import com.datadog.openfeature.internal.bridge.v1.ConfigurationSource; import datadog.trace.api.featureflag.FeatureFlaggingGateway; +import java.util.Set; import java.util.function.Consumer; /** Receives the flag configuration from the agent Remote Configuration client. */ @@ -16,4 +17,9 @@ public final class AgentConfigurationSource implements ConfigurationSource { public AutoCloseable subscribe(final Consumer listener) { return this.backend.subscribeRemoteConfig(listener); } + + @Override + public AutoCloseable subscribe(final Consumer listener, final Set capabilities) { + return this.backend.subscribeRemoteConfig(listener, capabilities); + } } diff --git a/dd-openfeature/src/main/java/com/datadog/openfeature/internal/bridge/v1/ConfigurationSource.java b/dd-openfeature/src/main/java/com/datadog/openfeature/internal/bridge/v1/ConfigurationSource.java index 79505f916c1..cd7bf44d991 100644 --- a/dd-openfeature/src/main/java/com/datadog/openfeature/internal/bridge/v1/ConfigurationSource.java +++ b/dd-openfeature/src/main/java/com/datadog/openfeature/internal/bridge/v1/ConfigurationSource.java @@ -1,5 +1,6 @@ package com.datadog.openfeature.internal.bridge.v1; +import java.util.Set; import java.util.function.Consumer; /** A source of Universal Flag Configuration (UFC) documents. */ @@ -13,4 +14,15 @@ public interface ConfigurationSource { * @return the handle to close to unsubscribe. */ AutoCloseable subscribe(Consumer listener); + + /** + * Subscribes with the evaluator's optional UFC capabilities. Older agents remain conservative. + * + * @param listener the configuration listener. + * @param capabilities features supported by evaluation and exposure reporting. + * @return the subscription handle. + */ + default AutoCloseable subscribe(Consumer listener, Set capabilities) { + return subscribe(listener); + } } diff --git a/dd-openfeature/src/main/java/com/datadog/openfeature/internal/delivery/CdnConfigurationSource.java b/dd-openfeature/src/main/java/com/datadog/openfeature/internal/delivery/CdnConfigurationSource.java index 7eed50bb8d2..fc0737e6cc4 100644 --- a/dd-openfeature/src/main/java/com/datadog/openfeature/internal/delivery/CdnConfigurationSource.java +++ b/dd-openfeature/src/main/java/com/datadog/openfeature/internal/delivery/CdnConfigurationSource.java @@ -5,6 +5,7 @@ import com.datadog.openfeature.internal.ConfigurationService; import com.datadog.openfeature.internal.RuntimeServices; import com.datadog.openfeature.internal.config.Settings; +import com.datadog.openfeature.internal.evaluation.EvaluatorCapabilities; import com.datadog.openfeature.internal.ufc.ServerConfiguration; import com.datadog.openfeature.internal.ufc.UniversalFlagConfigParser; import java.io.IOException; @@ -16,6 +17,7 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -35,6 +37,7 @@ public final class CdnConfigurationSource implements ConfigurationService { static final String UFC_RULES_BASED_SERVER_PATH = "/api/v2/feature-flagging/config/rules-based/server"; + static final String DEPENDENT_FLAG_VARIANT_KEY_QUERY = "cap_dependent-flags-variant-key=v1"; static final int MAX_ATTEMPTS = 3; private static final long FIRST_RETRY_MIN_MILLIS = 2_000; private static final long FIRST_RETRY_MAX_MILLIS = 10_000; @@ -124,7 +127,8 @@ public void start(final Consumer listener) { } // Logged last: a diagnostic must not keep the polling from starting. LOGGER.debug( - "Feature Flags agentless configuration source started: endpoint={}://{}{}, pollIntervalMillis={}, requestTimeout={}", + "Feature Flags agentless configuration source started: endpoint={}://{}{}," + + " pollIntervalMillis={}, requestTimeout={}", // The origin only: the userinfo, the path and the query of a custom endpoint can carry a // credential. this.endpoint.getScheme(), @@ -243,14 +247,16 @@ private boolean apply(final HttpResponse response) { configuration = UniversalFlagConfigParser.parseJsonApi(Gzip.decode(response)); } catch (final IOException | RuntimeException e) { LOGGER.debug( - "Feature Flags configuration endpoint returned a malformed payload ({}); retaining the current configuration", + "Feature Flags configuration endpoint returned a malformed payload ({}); retaining the" + + " current configuration", e.getClass().getSimpleName(), e); return false; } if (configuration == null) { LOGGER.debug( - "Feature Flags configuration endpoint returned no flag data; retaining the current configuration"); + "Feature Flags configuration endpoint returned no flag data; retaining the current" + + " configuration"); return false; } this.listener.accept(configuration); @@ -294,7 +300,7 @@ private HttpResponse fetchWithRetries() throws IOException, InterruptedE private HttpRequest buildRequest() { final HttpRequest.Builder request = - HttpRequest.newBuilder(this.endpoint) + HttpRequest.newBuilder(requestEndpoint(this.endpoint, EvaluatorCapabilities.supported())) .timeout(this.requestTimeout) .header("Accept-Encoding", "gzip"); Headers.addMetadata(request); @@ -308,6 +314,21 @@ private HttpRequest buildRequest() { return request.GET().build(); } + static URI requestEndpoint(final URI endpoint, final Set capabilities) { + if (!capabilities.contains(EvaluatorCapabilities.DEPENDENT_FLAG_VARIANT_KEY_V1)) { + return endpoint; + } + final String separator; + if (endpoint.getRawQuery() == null) { + separator = "?"; + } else if (endpoint.getRawQuery().isEmpty()) { + separator = ""; + } else { + separator = "&"; + } + return URI.create(endpoint + separator + DEPENDENT_FLAG_VARIANT_KEY_QUERY); + } + private synchronized void warn(final String message, final Object... arguments) { final long now = System.nanoTime(); if (this.lastWarningNanos == 0 || now - this.lastWarningNanos >= WARNING_INTERVAL_NANOS) { diff --git a/dd-openfeature/src/main/java/com/datadog/openfeature/internal/delivery/RemoteConfigurationService.java b/dd-openfeature/src/main/java/com/datadog/openfeature/internal/delivery/RemoteConfigurationService.java index ec63935162a..16055f00129 100644 --- a/dd-openfeature/src/main/java/com/datadog/openfeature/internal/delivery/RemoteConfigurationService.java +++ b/dd-openfeature/src/main/java/com/datadog/openfeature/internal/delivery/RemoteConfigurationService.java @@ -2,6 +2,7 @@ import com.datadog.openfeature.internal.ConfigurationService; import com.datadog.openfeature.internal.bridge.v1.ConfigurationSource; +import com.datadog.openfeature.internal.evaluation.EvaluatorCapabilities; import com.datadog.openfeature.internal.ufc.ServerConfiguration; import com.datadog.openfeature.internal.ufc.UniversalFlagConfigParser; import java.io.IOException; @@ -38,7 +39,8 @@ public synchronized void start(final Consumer listener) { configuration = UniversalFlagConfigParser.parse(content); } catch (final IOException | RuntimeException e) { LOGGER.debug( - "Remote Configuration returned a malformed flag configuration ({}); retaining the current configuration", + "Remote Configuration returned a malformed flag configuration ({}); retaining" + + " the current configuration", e.getClass().getSimpleName(), e); // Rejects the document so the agent reports it as not applied. @@ -48,7 +50,8 @@ public synchronized void start(final Consumer listener) { if (configuration != null) { listener.accept(configuration); } - }); + }, + EvaluatorCapabilities.supported()); } @Override diff --git a/dd-openfeature/src/main/java/com/datadog/openfeature/internal/evaluation/EvaluatorCapabilities.java b/dd-openfeature/src/main/java/com/datadog/openfeature/internal/evaluation/EvaluatorCapabilities.java new file mode 100644 index 00000000000..d4ea1e64a28 --- /dev/null +++ b/dd-openfeature/src/main/java/com/datadog/openfeature/internal/evaluation/EvaluatorCapabilities.java @@ -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 supported() { + return emptySet(); + } +} diff --git a/dd-openfeature/src/test/java/com/datadog/openfeature/internal/delivery/CdnConfigurationSourceTest.java b/dd-openfeature/src/test/java/com/datadog/openfeature/internal/delivery/CdnConfigurationSourceTest.java index 9dc8168dd5c..332cea215bb 100644 --- a/dd-openfeature/src/test/java/com/datadog/openfeature/internal/delivery/CdnConfigurationSourceTest.java +++ b/dd-openfeature/src/test/java/com/datadog/openfeature/internal/delivery/CdnConfigurationSourceTest.java @@ -27,6 +27,7 @@ import java.net.URI; import java.net.http.HttpClient; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -166,6 +167,16 @@ void sendsApiKeyAndMetadataHeaders() throws Exception { assertNull(request.header("If-None-Match")); } + @Test + void addsDependentFlagCapabilityToAgentlessQuery() { + assertEquals( + "https://example.com/ufc?token=a%2Bb&cap_dependent-flags-variant-key=v1", + CdnConfigurationSource.requestEndpoint( + URI.create("https://example.com/ufc?token=a%2Bb"), + Collections.singleton("dependent-flag-variant-key-v1")) + .toString()); + } + @Test void appliesJsonApiConfiguration() { this.server.enqueue(200, CONFIG, "ETag", "\"v1\""); diff --git a/dd-openfeature/src/test/java/com/datadog/openfeature/internal/evaluation/EvaluatorCapabilitiesTest.java b/dd-openfeature/src/test/java/com/datadog/openfeature/internal/evaluation/EvaluatorCapabilitiesTest.java new file mode 100644 index 00000000000..41bdfaec5c7 --- /dev/null +++ b/dd-openfeature/src/test/java/com/datadog/openfeature/internal/evaluation/EvaluatorCapabilitiesTest.java @@ -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()); + } +} diff --git a/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/FeatureFlaggingGateway.java b/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/FeatureFlaggingGateway.java index 7b6d9730548..2c0be453448 100644 --- a/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/FeatureFlaggingGateway.java +++ b/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/FeatureFlaggingGateway.java @@ -1,6 +1,7 @@ package datadog.trace.api.featureflag; import java.io.IOException; +import java.util.Set; import java.util.function.Consumer; import javax.annotation.Nullable; @@ -58,6 +59,12 @@ public interface Backend { */ AutoCloseable subscribeRemoteConfig(Consumer listener); + /** Subscribes with the SDK evaluator's optional UFC capabilities. */ + default AutoCloseable subscribeRemoteConfig( + Consumer listener, Set capabilities) { + return subscribeRemoteConfig(listener); + } + /** * @return whether the Datadog Agent event platform proxy is available. */ diff --git a/products/feature-flagging/feature-flagging-bootstrap/src/test/java/datadog/trace/api/featureflag/FeatureFlaggingGatewayTest.java b/products/feature-flagging/feature-flagging-bootstrap/src/test/java/datadog/trace/api/featureflag/FeatureFlaggingGatewayTest.java index 73bf6c24170..bf4827563dd 100644 --- a/products/feature-flagging/feature-flagging-bootstrap/src/test/java/datadog/trace/api/featureflag/FeatureFlaggingGatewayTest.java +++ b/products/feature-flagging/feature-flagging-bootstrap/src/test/java/datadog/trace/api/featureflag/FeatureFlaggingGatewayTest.java @@ -2,8 +2,13 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import java.util.Collections; +import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -22,4 +27,19 @@ void registersAndUnregistersTheBackend() { FeatureFlaggingGateway.register(null); assertNull(FeatureFlaggingGateway.backend()); } + + @Test + void capabilitySubscriptionDefaultsToTheCompatibleSubscriptionMethod() { + final FeatureFlaggingGateway.Backend backend = + mock(FeatureFlaggingGateway.Backend.class, CALLS_REAL_METHODS); + @SuppressWarnings("unchecked") + final Consumer listener = mock(Consumer.class); + final AutoCloseable subscription = mock(AutoCloseable.class); + doReturn(subscription).when(backend).subscribeRemoteConfig(listener); + + assertSame( + subscription, + backend.subscribeRemoteConfig(listener, Collections.singleton("optional-capability"))); + verify(backend).subscribeRemoteConfig(listener); + } } diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java index d6f9a72cb78..e8cbe6fc323 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/FeatureFlagsBackend.java @@ -14,6 +14,7 @@ import datadog.trace.api.telemetry.CoreMetricCollector; import java.io.IOException; import java.util.List; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -121,6 +122,12 @@ public AutoCloseable subscribeRemoteConfig(final Consumer listener) { return remoteConfig().subscribe(listener); } + @Override + public AutoCloseable subscribeRemoteConfig( + final Consumer listener, final Set capabilities) { + return remoteConfig().subscribe(listener, capabilities); + } + private synchronized RemoteConfigServiceImpl remoteConfig() { if (this.remoteConfig == null) { final ConfigurationPoller poller = this.sco.configurationPoller(this.config); diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java index 8922955d373..d788621013d 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java @@ -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, Boolean> dependentFlagSubscribers = new IdentityHashMap<>(); + private boolean dependentFlagsAdvertised; 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 subscriber) { + return subscribe(subscriber, emptySet()); + } + + /** Subscribes with the evaluator capabilities reported by this SDK instance. */ + public AutoCloseable subscribe( + final Consumer subscriber, final Set capabilities) { start(); + // A distinct forwarding listener keeps repeated registrations independently removable. + final Consumer 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,40 @@ public AutoCloseable subscribe(final Consumer 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; + 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); + } + requestAgain(); } /** @@ -114,7 +165,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; } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java index 9fcbdc11b91..a126e9357ff 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/FeatureFlagsBackendTest.java @@ -1,9 +1,11 @@ package com.datadog.featureflag; +import static datadog.remoteconfig.Capabilities.CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1; import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE; import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_ENABLED; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; +import static java.util.Collections.singleton; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -209,6 +211,20 @@ void sharesOneRemoteConfigServiceAcrossSubscriptions() { verify(poller, times(1)).addCapabilities(anyLong()); } + @Test + void capabilitySubscriptionDelegatesCapabilitiesToRemoteConfig() throws Exception { + final ConfigurationPoller poller = mock(ConfigurationPoller.class); + when(this.sco.configurationPoller(this.config)).thenReturn(poller); + + final AutoCloseable subscription = + this.backend.subscribeRemoteConfig( + content -> {}, singleton("dependent-flag-variant-key-v1")); + + verify(poller).addCapabilities(CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1); + subscription.close(); + verify(poller).removeCapabilities(CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1); + } + @Test void failsToSubscribeWithoutRemoteConfig() { assertThrows( diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/RemoteConfigServiceImplTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/RemoteConfigServiceImplTest.java index f4083331c6c..75daa146f0a 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/RemoteConfigServiceImplTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/RemoteConfigServiceImplTest.java @@ -27,6 +27,7 @@ import datadog.remoteconfig.PollingRateHinter; import datadog.remoteconfig.Product; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -45,6 +46,39 @@ class RemoteConfigServiceImplTest { private static final byte[] DOCUMENT = "{\"flags\":{}}".getBytes(UTF_8); + @Test + void gatesDependentFlagsOnEverySubscriberAndRemovesOnClose() throws Exception { + final ConfigurationPoller poller = mock(ConfigurationPoller.class); + final RemoteConfigServiceImpl service = new RemoteConfigServiceImpl(poller); + service.start(); + verify(poller, never()) + .addCapabilities(Capabilities.CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1); + final AutoCloseable supported = + service.subscribe(content -> {}, Collections.singleton("dependent-flag-variant-key-v1")); + verify(poller).addCapabilities(Capabilities.CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1); + final AutoCloseable legacy = service.subscribe(content -> {}); + verify(poller) + .removeCapabilities(Capabilities.CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1); + legacy.close(); + verify(poller, times(2)) + .addCapabilities(Capabilities.CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1); + supported.close(); + verify(poller, times(2)) + .removeCapabilities(Capabilities.CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1); + verify(poller, times(4)).removeListeners(Product.FFE_FLAGS); + service.close(); + } + + @Test + void unknownEvaluatorCapabilitiesDoNotOptIn() { + final ConfigurationPoller poller = mock(ConfigurationPoller.class); + final RemoteConfigServiceImpl service = new RemoteConfigServiceImpl(poller); + service.subscribe(content -> {}, Collections.singleton("unknown-v1")); + verify(poller, never()) + .addCapabilities(Capabilities.CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1); + service.close(); + } + @Test @SuppressWarnings("unchecked") void subscriptionRegistersTheProductWithARawDeserializer() throws Exception { diff --git a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java index ce76e59000a..d0956d08364 100644 --- a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java +++ b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java @@ -47,4 +47,5 @@ public interface Capabilities { long CAPABILITY_ASM_EXTENDED_DATA_COLLECTION = 1L << 44; long CAPABILITY_APM_TRACING_MULTICONFIG = 1L << 45; long CAPABILITY_FFE_FLAG_CONFIGURATION_RULES = 1L << 46; + long CAPABILITY_FFE_FLAG_DEPENDENT_FLAG_VARIANT_KEY_V1 = 1L << 50; }