Skip to content

Commit f9e70e6

Browse files
committed
Select the Feature Flagging configuration source
1 parent d378350 commit f9e70e6

5 files changed

Lines changed: 246 additions & 18 deletions

File tree

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,9 @@ public static void shutdown(final boolean sync) {
528528
if (flareEnabled) {
529529
stopFlarePoller();
530530
}
531+
if (featureFlaggingEnabled) {
532+
shutdownFeatureFlagging(AGENT_CLASSLOADER);
533+
}
531534

532535
if (agentlessLogSubmissionEnabled) {
533536
shutdownLogsIntake();
@@ -1179,6 +1182,20 @@ private static void maybeStartFeatureFlagging(final Class<?> scoClass, final Obj
11791182
}
11801183
}
11811184

1185+
static void shutdownFeatureFlagging(final ClassLoader agentClassLoader) {
1186+
if (agentClassLoader == null) {
1187+
return;
1188+
}
1189+
try {
1190+
final Class<?> ffSysClass =
1191+
agentClassLoader.loadClass("com.datadog.featureflag.FeatureFlaggingSystem");
1192+
final Method stopMethod = ffSysClass.getMethod("stop");
1193+
stopMethod.invoke(null);
1194+
} catch (final Throwable e) {
1195+
log.warn("Unable to stop Feature Flagging subsystem", e);
1196+
}
1197+
}
1198+
11821199
private static void maybeInstallLogsIntake(Class<?> scoClass, Object sco) {
11831200
if (agentlessLogSubmissionEnabled || appLogsCollectionEnabled) {
11841201
StaticEventLogger.begin("Logs Intake");
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package datadog.trace.bootstrap;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.concurrent.atomic.AtomicInteger;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.Test;
8+
9+
class AgentFeatureFlaggingLifecycleTest {
10+
11+
@BeforeEach
12+
void reset() {
13+
FakeFeatureFlaggingSystem.stopCalls.set(0);
14+
}
15+
16+
@Test
17+
void shutdownInvokesFeatureFlaggingSystemStopThroughAgentClassLoader() {
18+
final ClassLoader classLoader =
19+
new ClassLoader(null) {
20+
@Override
21+
public Class<?> loadClass(final String name) throws ClassNotFoundException {
22+
if ("com.datadog.featureflag.FeatureFlaggingSystem".equals(name)) {
23+
return FakeFeatureFlaggingSystem.class;
24+
}
25+
return super.loadClass(name);
26+
}
27+
};
28+
29+
Agent.shutdownFeatureFlagging(classLoader);
30+
31+
assertEquals(1, FakeFeatureFlaggingSystem.stopCalls.get());
32+
}
33+
34+
@Test
35+
void shutdownIsNoopBeforeAgentClassLoaderExists() {
36+
Agent.shutdownFeatureFlagging(null);
37+
38+
assertEquals(0, FakeFeatureFlaggingSystem.stopCalls.get());
39+
}
40+
41+
public static final class FakeFeatureFlaggingSystem {
42+
private static final AtomicInteger stopCalls = new AtomicInteger();
43+
44+
public static void stop() {
45+
stopCalls.incrementAndGet();
46+
}
47+
}
48+
}

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

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,93 @@ public class FeatureFlaggingSystem {
1414

1515
private FeatureFlaggingSystem() {}
1616

17-
public static void start(final SharedCommunicationObjects sco) {
17+
public static synchronized void start(final SharedCommunicationObjects sco) {
18+
if (CONFIG_SERVICE != null || EXPOSURE_WRITER != null) {
19+
LOGGER.debug("Feature Flagging system already started");
20+
return;
21+
}
1822
LOGGER.debug("Feature Flagging system starting");
1923
final Config config = Config.get();
24+
final ConfigurationSourceService configService = createConfigurationSourceService(sco, config);
25+
final ExposureWriter exposureWriter = new ExposureWriterImpl(sco, config);
26+
initialize(configService, exposureWriter);
27+
28+
LOGGER.debug("Feature Flagging system started");
29+
}
2030

21-
if (!config.isRemoteConfigEnabled()) {
22-
throw new IllegalStateException("Feature Flagging system started without RC");
31+
static void initialize(
32+
final ConfigurationSourceService configService, final ExposureWriter exposureWriter) {
33+
try {
34+
if (configService != null) {
35+
configService.init();
36+
}
37+
exposureWriter.init();
38+
CONFIG_SERVICE = configService;
39+
EXPOSURE_WRITER = exposureWriter;
40+
} catch (final RuntimeException | Error e) {
41+
exposureWriter.close();
42+
if (configService != null) {
43+
configService.close();
44+
}
45+
throw e;
2346
}
24-
CONFIG_SERVICE = new RemoteConfigServiceImpl(sco, config);
25-
CONFIG_SERVICE.init();
47+
}
2648

27-
EXPOSURE_WRITER = new ExposureWriterImpl(sco, config);
28-
EXPOSURE_WRITER.init();
49+
static ConfigurationSourceService createConfigurationSourceService(
50+
final SharedCommunicationObjects sco, final Config config) {
51+
final ConfigurationSource configurationSource =
52+
ConfigurationSource.from(config.getFeatureFlaggingConfigurationSource());
2953

30-
LOGGER.debug("Feature Flagging system started");
54+
if (configurationSource == ConfigurationSource.REMOTE_CONFIG) {
55+
if (!config.isRemoteConfigEnabled()) {
56+
throw new IllegalStateException("Feature Flagging system started without RC");
57+
}
58+
return new RemoteConfigServiceImpl(sco, config);
59+
}
60+
if (configurationSource == ConfigurationSource.AGENTLESS) {
61+
return new AgentlessConfigurationSource(config);
62+
}
63+
LOGGER.debug(
64+
"Feature Flagging offline configuration source selected; no config service started");
65+
return null;
3166
}
3267

33-
public static void stop() {
34-
if (EXPOSURE_WRITER != null) {
35-
EXPOSURE_WRITER.close();
36-
EXPOSURE_WRITER = null;
37-
}
38-
if (CONFIG_SERVICE != null) {
39-
CONFIG_SERVICE.close();
40-
CONFIG_SERVICE = null;
68+
public static synchronized void stop() {
69+
final ExposureWriter exposureWriter = EXPOSURE_WRITER;
70+
final ConfigurationSourceService configService = CONFIG_SERVICE;
71+
EXPOSURE_WRITER = null;
72+
CONFIG_SERVICE = null;
73+
try {
74+
if (exposureWriter != null) {
75+
exposureWriter.close();
76+
}
77+
} finally {
78+
if (configService != null) {
79+
configService.close();
80+
}
4181
}
4282
LOGGER.debug("Feature Flagging system stopped");
4383
}
84+
85+
private enum ConfigurationSource {
86+
AGENTLESS("agentless"),
87+
REMOTE_CONFIG("remote_config"),
88+
OFFLINE("offline");
89+
90+
private final String value;
91+
92+
ConfigurationSource(final String value) {
93+
this.value = value;
94+
}
95+
96+
private static ConfigurationSource from(final String value) {
97+
for (final ConfigurationSource source : values()) {
98+
if (source.value.equals(value)) {
99+
return source;
100+
}
101+
}
102+
throw new IllegalArgumentException(
103+
"Unsupported Feature Flagging configuration source: " + value);
104+
}
105+
}
44106
}

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import static datadog.trace.api.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE;
44
import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIGURATION_ENABLED;
5+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
6+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
7+
import static org.junit.jupiter.api.Assertions.assertNull;
58
import static org.junit.jupiter.api.Assertions.assertThrows;
69
import static org.mockito.ArgumentMatchers.any;
710
import static org.mockito.ArgumentMatchers.eq;
11+
import static org.mockito.Mockito.doThrow;
812
import static org.mockito.Mockito.mock;
913
import static org.mockito.Mockito.verify;
1014
import static org.mockito.Mockito.when;
@@ -37,12 +41,14 @@ void testFeatureFlagSystemInitialization() {
3741
sharedCommunicationObjects.agentUrl = HttpUrl.get("http://localhost");
3842
sharedCommunicationObjects.agentHttpClient = new OkHttpClient.Builder().build();
3943

44+
FeatureFlaggingSystem.start(sharedCommunicationObjects);
4045
FeatureFlaggingSystem.start(sharedCommunicationObjects);
4146

4247
verify(poller).addCapabilities(Capabilities.CAPABILITY_FFE_FLAG_CONFIGURATION_RULES);
4348
verify(poller).addListener(eq(Product.FFE_FLAGS), any(ConfigurationDeserializer.class), any());
4449
verify(poller).start();
4550

51+
FeatureFlaggingSystem.stop();
4652
FeatureFlaggingSystem.stop();
4753

4854
verify(poller).removeCapabilities(Capabilities.CAPABILITY_FFE_FLAG_CONFIGURATION_RULES);
@@ -64,4 +70,93 @@ void testThatRemoteConfigIsRequired() {
6470
FeatureFlaggingSystem.stop();
6571
}
6672
}
73+
74+
@Test
75+
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "agentless")
76+
@WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "false")
77+
void agentlessConfigurationSourceUsesHttpServiceWithoutRemoteConfig() {
78+
assertInstanceOf(
79+
AgentlessConfigurationSource.class,
80+
FeatureFlaggingSystem.createConfigurationSourceService(
81+
sharedCommunicationObjects(), Config.get()));
82+
}
83+
84+
@Test
85+
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "remote_config")
86+
@WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true")
87+
void explicitRemoteConfigUsesRemoteConfigService() {
88+
SharedCommunicationObjects sharedCommunicationObjects = sharedCommunicationObjects();
89+
when(sharedCommunicationObjects.configurationPoller(any(Config.class)))
90+
.thenReturn(mock(ConfigurationPoller.class));
91+
92+
assertInstanceOf(
93+
RemoteConfigServiceImpl.class,
94+
FeatureFlaggingSystem.createConfigurationSourceService(
95+
sharedCommunicationObjects, Config.get()));
96+
}
97+
98+
@Test
99+
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "invalid")
100+
void invalidConfigurationSourceFailsBeforeStartingNetworkSource() {
101+
assertThrows(
102+
IllegalArgumentException.class,
103+
() ->
104+
FeatureFlaggingSystem.createConfigurationSourceService(
105+
sharedCommunicationObjects(), Config.get()));
106+
}
107+
108+
@Test
109+
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "offline")
110+
void offlineConfigurationSourceDoesNotStartNetworkSource() {
111+
assertNull(
112+
FeatureFlaggingSystem.createConfigurationSourceService(
113+
sharedCommunicationObjects(), Config.get()));
114+
}
115+
116+
@Test
117+
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "offline")
118+
void startWithOfflineConfigurationSourceSkipsConfigService() {
119+
try {
120+
assertDoesNotThrow(() -> FeatureFlaggingSystem.start(sharedCommunicationObjects()));
121+
} finally {
122+
FeatureFlaggingSystem.stop();
123+
}
124+
}
125+
126+
@Test
127+
void initializationFailureClosesConfigurationSourceAndExposureWriter() {
128+
ConfigurationSourceService configService = mock(ConfigurationSourceService.class);
129+
ExposureWriter exposureWriter = mock(ExposureWriter.class);
130+
doThrow(new IllegalStateException("exposure init failed")).when(exposureWriter).init();
131+
132+
assertThrows(
133+
IllegalStateException.class,
134+
() -> FeatureFlaggingSystem.initialize(configService, exposureWriter));
135+
136+
verify(configService).init();
137+
verify(configService).close();
138+
verify(exposureWriter).close();
139+
}
140+
141+
@Test
142+
void initializationFailureWithoutConfigurationSourceClosesExposureWriter() {
143+
ExposureWriter exposureWriter = mock(ExposureWriter.class);
144+
doThrow(new IllegalStateException("exposure init failed")).when(exposureWriter).init();
145+
146+
assertThrows(
147+
IllegalStateException.class, () -> FeatureFlaggingSystem.initialize(null, exposureWriter));
148+
149+
verify(exposureWriter).close();
150+
}
151+
152+
private static SharedCommunicationObjects sharedCommunicationObjects() {
153+
DDAgentFeaturesDiscovery discovery = mock(DDAgentFeaturesDiscovery.class);
154+
when(discovery.supportsEvpProxy()).thenReturn(true);
155+
when(discovery.getEvpProxyEndpoint()).thenReturn("/evp_proxy/");
156+
SharedCommunicationObjects sharedCommunicationObjects = mock(SharedCommunicationObjects.class);
157+
when(sharedCommunicationObjects.featuresDiscovery(any(Config.class))).thenReturn(discovery);
158+
sharedCommunicationObjects.agentUrl = HttpUrl.get("http://localhost");
159+
sharedCommunicationObjects.agentHttpClient = new OkHttpClient.Builder().build();
160+
return sharedCommunicationObjects;
161+
}
67162
}

products/feature-flagging/feature-flagging-api/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,11 @@ OTEL_EXPORTER_OTLP_PROTOCOL=grpc
8585
## Requirements
8686

8787
- Java 11+
88-
- Datadog Agent with Remote Configuration enabled
89-
- `DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true`
88+
- `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless` uses the Datadog agentless
89+
backend. Set `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL` to a
90+
different HTTP backend while keeping agentless delivery semantics. A bare
91+
host uses the standard server-distribution path; a URL with a path is used as
92+
the exact UFC endpoint. `remote_config` uses the existing Agent Remote
93+
Configuration path. `offline` is reserved for startup-provided UFC bytes;
94+
until those bytes are implemented, no network source starts and evaluations
95+
use defaults.

0 commit comments

Comments
 (0)