Skip to content

Commit 2c0ee6c

Browse files
committed
feat(feature-flags): delay agentless polling until provider use
1 parent eaa99ce commit 2c0ee6c

7 files changed

Lines changed: 168 additions & 39 deletions

File tree

products/feature-flagging/feature-flagging-agent/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies {
1717
api(libs.slf4j)
1818
api(project(":products:feature-flagging:feature-flagging-lib"))
1919
api(project(":internal-api"))
20+
compileOnly(project(":products:feature-flagging:feature-flagging-config"))
2021

2122
testImplementation(libs.bundles.junit5)
2223
testImplementation(libs.bundles.mockito)

products/feature-flagging/feature-flagging-agent/src/main/java/com/datadog/featureflag/FeatureFlaggingSystem.java

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.datadog.featureflag;
22

3+
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_AGENTLESS;
4+
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.CONFIGURATION_SOURCE_REMOTE_CONFIG;
5+
36
import datadog.communication.ddagent.SharedCommunicationObjects;
47
import datadog.trace.api.Config;
8+
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
59
import org.slf4j.Logger;
610
import org.slf4j.LoggerFactory;
711

@@ -12,17 +16,64 @@ public class FeatureFlaggingSystem {
1216
private static volatile ConfigurationSourceService CONFIG_SERVICE;
1317
private static volatile ExposureWriter EXPOSURE_WRITER;
1418
private static volatile SpanEnrichmentWriter SPAN_ENRICHMENT_WRITER;
19+
private static volatile FeatureFlaggingGateway.ActivationListener ACTIVATION_LISTENER;
20+
private static volatile boolean STARTED;
1521

1622
private FeatureFlaggingSystem() {}
1723

1824
public static synchronized void start(final SharedCommunicationObjects sco) {
19-
if (CONFIG_SERVICE != null || EXPOSURE_WRITER != null) {
25+
if (STARTED) {
2026
LOGGER.debug("Feature Flagging system already started");
2127
return;
2228
}
2329
LOGGER.debug("Feature Flagging system starting");
2430
final Config config = Config.get();
31+
STARTED = true;
32+
33+
if (!config.isFeatureFlaggingProviderEnabled()) {
34+
LOGGER.debug("Feature Flagging system disabled");
35+
return;
36+
}
37+
38+
if (CONFIGURATION_SOURCE_AGENTLESS.equals(config.getFeatureFlaggingConfigurationSource())) {
39+
final FeatureFlaggingGateway.ActivationListener activationListener =
40+
() -> activateAgentless(sco, config);
41+
ACTIVATION_LISTENER = activationListener;
42+
FeatureFlaggingGateway.addActivationListener(activationListener);
43+
LOGGER.debug("Feature Flagging system awaiting application provider activation");
44+
return;
45+
}
46+
47+
try {
48+
initializeSystem(sco, config);
49+
} catch (final RuntimeException | Error e) {
50+
STARTED = false;
51+
throw e;
52+
}
53+
}
54+
55+
private static synchronized void activateAgentless(
56+
final SharedCommunicationObjects sco, final Config config) {
57+
final FeatureFlaggingGateway.ActivationListener activationListener = ACTIVATION_LISTENER;
58+
if (!STARTED || activationListener == null) {
59+
return;
60+
}
61+
ACTIVATION_LISTENER = null;
62+
FeatureFlaggingGateway.removeActivationListener(activationListener);
63+
try {
64+
initializeSystem(sco, config);
65+
} catch (final RuntimeException | Error e) {
66+
STARTED = false;
67+
throw e;
68+
}
69+
}
70+
71+
private static void initializeSystem(final SharedCommunicationObjects sco, final Config config) {
2572
final ConfigurationSourceService configService = createConfigurationSourceService(sco, config);
73+
if (configService == null) {
74+
LOGGER.debug("Feature Flagging system disabled by unsupported configuration source");
75+
return;
76+
}
2677
final ExposureWriter exposureWriter = new ExposureWriterImpl(sco, config);
2778
initialize(configService, exposureWriter);
2879

@@ -60,30 +111,32 @@ static void initialize(
60111

61112
static ConfigurationSourceService createConfigurationSourceService(
62113
final SharedCommunicationObjects sco, final Config config) {
63-
final ConfigurationSource configurationSource =
64-
ConfigurationSource.from(config.getFeatureFlaggingConfigurationSource());
65-
66-
if (configurationSource == ConfigurationSource.REMOTE_CONFIG) {
114+
final String configurationSource = config.getFeatureFlaggingConfigurationSource();
115+
if (CONFIGURATION_SOURCE_REMOTE_CONFIG.equals(configurationSource)) {
67116
if (!config.isRemoteConfigEnabled()) {
68117
throw new IllegalStateException("Feature Flagging system started without RC");
69118
}
70119
return new RemoteConfigServiceImpl(sco, config);
71120
}
72-
if (configurationSource == ConfigurationSource.AGENTLESS) {
121+
if (CONFIGURATION_SOURCE_AGENTLESS.equals(configurationSource)) {
73122
return new AgentlessConfigurationSource(config);
74123
}
75-
LOGGER.debug(
76-
"Feature Flagging offline configuration source selected; no config service started");
77124
return null;
78125
}
79126

80127
public static synchronized void stop() {
128+
final FeatureFlaggingGateway.ActivationListener activationListener = ACTIVATION_LISTENER;
81129
final SpanEnrichmentWriter spanEnrichmentWriter = SPAN_ENRICHMENT_WRITER;
82130
final ExposureWriter exposureWriter = EXPOSURE_WRITER;
83131
final ConfigurationSourceService configService = CONFIG_SERVICE;
132+
STARTED = false;
133+
ACTIVATION_LISTENER = null;
84134
SPAN_ENRICHMENT_WRITER = null;
85135
EXPOSURE_WRITER = null;
86136
CONFIG_SERVICE = null;
137+
if (activationListener != null) {
138+
FeatureFlaggingGateway.removeActivationListener(activationListener);
139+
}
87140
try {
88141
if (spanEnrichmentWriter != null) {
89142
spanEnrichmentWriter.close();
@@ -102,25 +155,7 @@ public static synchronized void stop() {
102155
LOGGER.debug("Feature Flagging system stopped");
103156
}
104157

105-
private enum ConfigurationSource {
106-
AGENTLESS("agentless"),
107-
REMOTE_CONFIG("remote_config"),
108-
OFFLINE("offline");
109-
110-
private final String value;
111-
112-
ConfigurationSource(final String value) {
113-
this.value = value;
114-
}
115-
116-
private static ConfigurationSource from(final String value) {
117-
for (final ConfigurationSource source : values()) {
118-
if (source.value.equals(value)) {
119-
return source;
120-
}
121-
}
122-
throw new IllegalArgumentException(
123-
"Unsupported Feature Flagging configuration source: " + value);
124-
}
158+
static boolean isAwaitingApplicationActivation() {
159+
return ACTIVATION_LISTENER != null;
125160
}
126161
}

products/feature-flagging/feature-flagging-agent/src/test/java/com/datadog/featureflag/FeatureFlaggingSystemTest.java

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22

33
import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIGURATION_ENABLED;
44
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE;
5+
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL;
56
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
7+
import static org.junit.jupiter.api.Assertions.assertFalse;
68
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
79
import static org.junit.jupiter.api.Assertions.assertNull;
810
import static org.junit.jupiter.api.Assertions.assertThrows;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
912
import static org.mockito.ArgumentMatchers.any;
1013
import static org.mockito.ArgumentMatchers.eq;
14+
import static org.mockito.Mockito.clearInvocations;
1115
import static org.mockito.Mockito.doThrow;
1216
import static org.mockito.Mockito.mock;
1317
import static org.mockito.Mockito.verify;
18+
import static org.mockito.Mockito.verifyNoInteractions;
1419
import static org.mockito.Mockito.when;
1520

1621
import datadog.communication.ddagent.DDAgentFeaturesDiscovery;
@@ -20,13 +25,39 @@
2025
import datadog.remoteconfig.ConfigurationPoller;
2126
import datadog.remoteconfig.Product;
2227
import datadog.trace.api.Config;
28+
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
2329
import datadog.trace.test.junit.utils.config.WithConfig;
2430
import okhttp3.HttpUrl;
2531
import okhttp3.OkHttpClient;
2632
import org.junit.jupiter.api.Test;
2733

2834
class FeatureFlaggingSystemTest {
2935

36+
@Test
37+
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "agentless")
38+
@WithConfig(
39+
key = FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL,
40+
value = "http://127.0.0.1:1")
41+
void agentlessStartWaitsForApplicationProviderActivation() {
42+
SharedCommunicationObjects sharedCommunicationObjects = sharedCommunicationObjects();
43+
clearInvocations(sharedCommunicationObjects);
44+
45+
try {
46+
FeatureFlaggingSystem.start(sharedCommunicationObjects);
47+
48+
assertTrue(FeatureFlaggingSystem.isAwaitingApplicationActivation());
49+
verifyNoInteractions(sharedCommunicationObjects);
50+
51+
FeatureFlaggingGateway.activate();
52+
53+
assertFalse(FeatureFlaggingSystem.isAwaitingApplicationActivation());
54+
} finally {
55+
FeatureFlaggingSystem.stop();
56+
}
57+
58+
assertFalse(FeatureFlaggingSystem.isAwaitingApplicationActivation());
59+
}
60+
3061
@Test
3162
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "remote_config")
3263
@WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true")
@@ -97,23 +128,20 @@ void explicitRemoteConfigUsesRemoteConfigService() {
97128

98129
@Test
99130
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "invalid")
100-
void invalidConfigurationSourceUsesAgentlessDefault() {
101-
assertInstanceOf(
102-
AgentlessConfigurationSource.class,
131+
void invalidConfigurationSourceDoesNotStartNetworkSource() {
132+
assertNull(
103133
FeatureFlaggingSystem.createConfigurationSourceService(
104134
sharedCommunicationObjects(), Config.get()));
105135
}
106136

107137
@Test
108-
void rejectsUnsupportedNormalizedConfigurationSource() {
138+
void unsupportedNormalizedConfigurationSourceDoesNotStartNetworkSource() {
109139
Config config = mock(Config.class);
110140
when(config.getFeatureFlaggingConfigurationSource()).thenReturn("invalid");
111141

112-
assertThrows(
113-
IllegalArgumentException.class,
114-
() ->
115-
FeatureFlaggingSystem.createConfigurationSourceService(
116-
sharedCommunicationObjects(), config));
142+
assertNull(
143+
FeatureFlaggingSystem.createConfigurationSourceService(
144+
sharedCommunicationObjects(), config));
117145
}
118146

119147
@Test
@@ -126,9 +154,25 @@ void offlineConfigurationSourceDoesNotStartNetworkSource() {
126154

127155
@Test
128156
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "offline")
129-
void startWithOfflineConfigurationSourceSkipsConfigService() {
157+
void startWithOfflineConfigurationSourceDisablesSystem() {
158+
SharedCommunicationObjects sharedCommunicationObjects = mock(SharedCommunicationObjects.class);
159+
160+
try {
161+
assertDoesNotThrow(() -> FeatureFlaggingSystem.start(sharedCommunicationObjects));
162+
verifyNoInteractions(sharedCommunicationObjects);
163+
} finally {
164+
FeatureFlaggingSystem.stop();
165+
}
166+
}
167+
168+
@Test
169+
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "invalid")
170+
void startWithInvalidConfigurationSourceDisablesSystem() {
171+
SharedCommunicationObjects sharedCommunicationObjects = mock(SharedCommunicationObjects.class);
172+
130173
try {
131-
assertDoesNotThrow(() -> FeatureFlaggingSystem.start(sharedCommunicationObjects()));
174+
assertDoesNotThrow(() -> FeatureFlaggingSystem.start(sharedCommunicationObjects));
175+
verifyNoInteractions(sharedCommunicationObjects);
132176
} finally {
133177
FeatureFlaggingSystem.stop();
134178
}

products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public DDEvaluator(final Runnable configCallback) {
6767
@Override
6868
public boolean initialize(
6969
final long timeout, final TimeUnit unit, final EvaluationContext context) throws Exception {
70+
FeatureFlaggingGateway.activate();
7071
FeatureFlaggingGateway.addConfigListener(this);
7172
return initializationLatch.await(timeout, unit) || hasConfiguration();
7273
}

products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.squareup.moshi.JsonWriter;
2424
import com.squareup.moshi.Moshi;
2525
import com.squareup.moshi.Types;
26+
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
2627
import datadog.trace.api.featureflag.ufc.v1.Flag;
2728
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
2829
import dev.openfeature.sdk.ErrorCode;
@@ -64,6 +65,22 @@ public class DDEvaluatorTest {
6465
private static final JsonAdapter<List<FixtureCase>> FIXTURE_LIST_ADAPTER =
6566
MOSHI.adapter(FIXTURE_LIST_TYPE);
6667

68+
@Test
69+
public void testInitializeSignalsApplicationProviderActivation() throws Exception {
70+
final FeatureFlaggingGateway.ActivationListener listener =
71+
mock(FeatureFlaggingGateway.ActivationListener.class);
72+
final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class));
73+
FeatureFlaggingGateway.addActivationListener(listener);
74+
try {
75+
evaluator.initialize(1, MILLISECONDS, mock(EvaluationContext.class));
76+
77+
verify(listener).activate();
78+
} finally {
79+
evaluator.shutdown();
80+
FeatureFlaggingGateway.removeActivationListener(listener);
81+
}
82+
}
83+
6784
private static Arguments[] valueMappingTestCases() {
6885
return new Arguments[] {
6986
// String mappings

products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/FeatureFlaggingGateway.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@ public abstract class FeatureFlaggingGateway {
1111

1212
public interface ConfigListener extends Consumer<ServerConfiguration> {}
1313

14+
public interface ActivationListener {
15+
void activate();
16+
}
17+
1418
public interface ExposureListener extends Consumer<ExposureEvent> {}
1519

1620
public interface SpanEnrichmentListener extends Consumer<SpanEnrichmentEvent> {}
1721

1822
private static final List<ConfigListener> CONFIG_LISTENERS = new CopyOnWriteArrayList<>();
23+
private static final List<ActivationListener> ACTIVATION_LISTENERS = new CopyOnWriteArrayList<>();
1924
private static final List<ExposureListener> EXPOSURE_LISTENERS = new CopyOnWriteArrayList<>();
2025
private static final List<SpanEnrichmentListener> SPAN_ENRICHMENT_LISTENERS =
2126
new CopyOnWriteArrayList<>();
@@ -42,6 +47,19 @@ public static void dispatch(final ServerConfiguration config) {
4247
CONFIG_LISTENERS.forEach(listener -> listener.accept(config));
4348
}
4449

50+
public static void addActivationListener(final ActivationListener listener) {
51+
ACTIVATION_LISTENERS.add(listener);
52+
}
53+
54+
public static void removeActivationListener(final ActivationListener listener) {
55+
ACTIVATION_LISTENERS.remove(listener);
56+
}
57+
58+
/** Signals that application code initialized the Datadog OpenFeature provider. */
59+
public static void activate() {
60+
ACTIVATION_LISTENERS.forEach(ActivationListener::activate);
61+
}
62+
4563
public static void addExposureListener(final ExposureListener listener) {
4664
EXPOSURE_LISTENERS.add(listener);
4765
}

products/feature-flagging/feature-flagging-bootstrap/src/test/java/datadog/trace/api/featureflag/FeatureFlaggingGatewayTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
class FeatureFlaggingGatewayTest {
1414

1515
private FeatureFlaggingGateway.ConfigListener configListener;
16+
private FeatureFlaggingGateway.ActivationListener activationListener;
1617
private FeatureFlaggingGateway.ExposureListener exposureListener;
1718
private FeatureFlaggingGateway.SpanEnrichmentListener spanEnrichmentListener;
1819
private ServerConfiguration firstConfiguration;
@@ -23,6 +24,7 @@ class FeatureFlaggingGatewayTest {
2324
@BeforeEach
2425
void setUp() {
2526
configListener = mock(FeatureFlaggingGateway.ConfigListener.class);
27+
activationListener = mock(FeatureFlaggingGateway.ActivationListener.class);
2628
exposureListener = mock(FeatureFlaggingGateway.ExposureListener.class);
2729
spanEnrichmentListener = mock(FeatureFlaggingGateway.SpanEnrichmentListener.class);
2830
firstConfiguration = mock(ServerConfiguration.class);
@@ -34,10 +36,21 @@ void setUp() {
3436
@AfterEach
3537
void tearDown() {
3638
FeatureFlaggingGateway.removeConfigListener(configListener);
39+
FeatureFlaggingGateway.removeActivationListener(activationListener);
3740
FeatureFlaggingGateway.removeExposureListener(exposureListener);
3841
FeatureFlaggingGateway.removeSpanEnrichmentListener(spanEnrichmentListener);
3942
}
4043

44+
@Test
45+
void testProviderActivationListener() {
46+
FeatureFlaggingGateway.addActivationListener(activationListener);
47+
48+
FeatureFlaggingGateway.activate();
49+
50+
verify(activationListener).activate();
51+
verifyNoMoreInteractions(activationListener);
52+
}
53+
4154
@Test
4255
void testAttachingAConfigListener() {
4356
clearCurrentServerConfiguration();

0 commit comments

Comments
 (0)