Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServerConfiguration> {}

public interface ActivationListener {
Expand All @@ -29,6 +35,8 @@ public interface SpanEnrichmentListener extends Consumer<SpanEnrichmentEvent> {}
private static final AtomicReference<ServerConfiguration> CURRENT_CONFIG =
new AtomicReference<>();

private static final AtomicReference<RuntimeMode> 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
Expand Down Expand Up @@ -72,6 +80,28 @@ public static void activate() {
ACTIVATION_LISTENERS.forEach(ActivationListener::activate);
}

/**
* Claims process-wide ownership of Feature Flagging configuration and event delivery.
*
* <p>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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down