diff --git a/products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java b/products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java index 7f105d00213..9791594e7f8 100644 --- a/products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java +++ b/products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java @@ -6,6 +6,7 @@ import datadog.communication.ddagent.SharedCommunicationObjects; import datadog.trace.api.Config; import datadog.trace.api.featureflag.FeatureFlaggingGateway; +import datadog.trace.api.featureflag.FeatureFlaggingGateway.RuntimeMode; import datadog.trace.api.featureflag.config.FeatureFlaggingConfig; import datadog.trace.api.featureflag.flagevaluation.FlagEvaluationWriter; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -80,6 +81,12 @@ private static void initializeOrRollBack( final SharedCommunicationObjects sco, final Config config, final SystemInitializer systemInitializer) { + if (!FeatureFlaggingGateway.claimRuntime(RuntimeMode.AGENT)) { + LOGGER.debug( + "Feature Flagging agent runtime not started because {} already owns the subsystem", + FeatureFlaggingGateway.activeRuntime()); + return; + } try { systemInitializer.initialize(sco, config); } catch (final RuntimeException | Error e) { @@ -180,6 +187,7 @@ public static synchronized void stop() { SPAN_ENRICHMENT_WRITER = null; EXPOSURE_WRITER = null; CONFIG_SERVICE = null; + FeatureFlaggingGateway.releaseRuntime(RuntimeMode.AGENT); if (activationListener != null) { FeatureFlaggingGateway.removeActivationListener(activationListener); } diff --git a/products/feature-flagging/feature-flagging-agent/src/test/java/com/datadog/featureflag/FeatureFlaggingSystemTest.java b/products/feature-flagging/feature-flagging-agent/src/test/java/com/datadog/featureflag/FeatureFlaggingSystemTest.java index 5b88389aefc..69303d56de2 100644 --- a/products/feature-flagging/feature-flagging-agent/src/test/java/com/datadog/featureflag/FeatureFlaggingSystemTest.java +++ b/products/feature-flagging/feature-flagging-agent/src/test/java/com/datadog/featureflag/FeatureFlaggingSystemTest.java @@ -28,6 +28,7 @@ import datadog.remoteconfig.Product; import datadog.trace.api.Config; import datadog.trace.api.featureflag.FeatureFlaggingGateway; +import datadog.trace.api.featureflag.FeatureFlaggingGateway.RuntimeMode; import datadog.trace.api.featureflag.config.FeatureFlaggingConfig; import datadog.trace.api.featureflag.flagevaluation.FlagEvaluationWriter; import datadog.trace.test.junit.utils.config.WithConfig; @@ -86,6 +87,25 @@ void agentlessActivationInitializesSystemOnce() { verify(systemInitializer).initialize(eq(sharedCommunicationObjects), any(Config.class)); assertFalse(FeatureFlaggingSystem.isAwaitingApplicationActivation()); + assertSame(RuntimeMode.AGENT, FeatureFlaggingGateway.activeRuntime()); + } + + @Test + @WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "agentless") + void agentlessActivationDoesNotStartWhenStandaloneRuntimeOwnsTheProcess() { + final SharedCommunicationObjects sharedCommunicationObjects = sharedCommunicationObjects(); + final FeatureFlaggingSystem.SystemInitializer systemInitializer = + mock(FeatureFlaggingSystem.SystemInitializer.class); + assertTrue(FeatureFlaggingGateway.claimRuntime(RuntimeMode.STANDALONE)); + try { + FeatureFlaggingSystem.start(sharedCommunicationObjects, systemInitializer); + FeatureFlaggingGateway.activate(); + + verifyNoInteractions(systemInitializer); + assertSame(RuntimeMode.STANDALONE, FeatureFlaggingGateway.activeRuntime()); + } finally { + FeatureFlaggingGateway.releaseRuntime(RuntimeMode.STANDALONE); + } } @Test @@ -170,6 +190,7 @@ void testFeatureFlagSystemInitialization() { FeatureFlaggingSystem.stop(); assertFalse(FeatureFlaggingGateway.isFlagEvaluationEnqueueEnabled()); assertNull(FeatureFlaggingGateway.getFlagEvalWriter()); + assertNull(FeatureFlaggingGateway.activeRuntime()); // stop() is idempotent: a second call must be a safe no-op. FeatureFlaggingSystem.stop(); 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 2a823bd32ef..dd4198ff227 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 @@ -4,12 +4,18 @@ import datadog.trace.api.featureflag.flagevaluation.FlagEvaluationWriter; import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration; import java.util.List; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; public abstract class FeatureFlaggingGateway { + public enum RuntimeMode { + AGENT, + STANDALONE + } + public interface ConfigListener extends Consumer {} public interface ActivationListener { @@ -29,6 +35,8 @@ public interface SpanEnrichmentListener extends Consumer {} private static final AtomicReference CURRENT_CONFIG = new AtomicReference<>(); + private static final AtomicReference ACTIVE_RUNTIME = new AtomicReference<>(); + /** * The active EVP flagevaluation writer. Registered by {@code FlagEvaluationWriterImpl.start()} * when the killswitch {@code DD_FLAGGING_EVALUATION_COUNTS_ENABLED} is on (default). Read by @@ -72,6 +80,28 @@ public static void activate() { ACTIVATION_LISTENERS.forEach(ActivationListener::activate); } + /** + * Claims process-wide ownership of Feature Flagging configuration and event delivery. + * + *

The claim is idempotent for the current owner. A different runtime must not start while an + * owner is active because doing so would create duplicate configuration pollers and duplicate + * exposure or evaluation delivery. + */ + public static boolean claimRuntime(final RuntimeMode runtime) { + Objects.requireNonNull(runtime, "runtime"); + return ACTIVE_RUNTIME.compareAndSet(null, runtime) || ACTIVE_RUNTIME.get() == runtime; + } + + /** Releases process-wide ownership when {@code runtime} is the current owner. */ + public static void releaseRuntime(final RuntimeMode runtime) { + ACTIVE_RUNTIME.compareAndSet(runtime, null); + } + + /** Returns the runtime currently responsible for configuration and event delivery. */ + public static RuntimeMode activeRuntime() { + return ACTIVE_RUNTIME.get(); + } + public static void addExposureListener(final ExposureListener listener) { EXPOSURE_LISTENERS.add(listener); } 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 887a153f0a1..7ce709801a5 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 @@ -1,5 +1,10 @@ package datadog.trace.api.featureflag; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -41,6 +46,8 @@ void tearDown() { FeatureFlaggingGateway.removeSpanEnrichmentListener(spanEnrichmentListener); FeatureFlaggingGateway.setFlagEvalWriter(null); FeatureFlaggingGateway.setFlagEvaluationEnqueueEnabled(true); + FeatureFlaggingGateway.releaseRuntime(FeatureFlaggingGateway.RuntimeMode.AGENT); + FeatureFlaggingGateway.releaseRuntime(FeatureFlaggingGateway.RuntimeMode.STANDALONE); } @Test @@ -53,6 +60,30 @@ void testProviderActivationListener() { verifyNoMoreInteractions(activationListener); } + @Test + void runtimeOwnershipIsExclusiveAndIdempotent() { + assertNull(FeatureFlaggingGateway.activeRuntime()); + + assertTrue(FeatureFlaggingGateway.claimRuntime(FeatureFlaggingGateway.RuntimeMode.STANDALONE)); + assertTrue(FeatureFlaggingGateway.claimRuntime(FeatureFlaggingGateway.RuntimeMode.STANDALONE)); + assertFalse(FeatureFlaggingGateway.claimRuntime(FeatureFlaggingGateway.RuntimeMode.AGENT)); + assertEquals( + FeatureFlaggingGateway.RuntimeMode.STANDALONE, FeatureFlaggingGateway.activeRuntime()); + + FeatureFlaggingGateway.releaseRuntime(FeatureFlaggingGateway.RuntimeMode.AGENT); + assertEquals( + FeatureFlaggingGateway.RuntimeMode.STANDALONE, FeatureFlaggingGateway.activeRuntime()); + + FeatureFlaggingGateway.releaseRuntime(FeatureFlaggingGateway.RuntimeMode.STANDALONE); + assertNull(FeatureFlaggingGateway.activeRuntime()); + assertTrue(FeatureFlaggingGateway.claimRuntime(FeatureFlaggingGateway.RuntimeMode.AGENT)); + } + + @Test + void runtimeOwnershipRejectsNull() { + assertThrows(NullPointerException.class, () -> FeatureFlaggingGateway.claimRuntime(null)); + } + @Test void testAttachingAConfigListener() { clearCurrentServerConfiguration();