Skip to content

Commit 88d966a

Browse files
committed
feat(ffe): defer agentless delivery until provider access
1 parent 5bd54ec commit 88d966a

7 files changed

Lines changed: 129 additions & 28 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: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import datadog.communication.ddagent.SharedCommunicationObjects;
44
import datadog.trace.api.Config;
5+
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
6+
import datadog.trace.api.featureflag.config.FeatureFlaggingConfigurationSource;
57
import org.slf4j.Logger;
68
import org.slf4j.LoggerFactory;
79

@@ -12,16 +14,61 @@ public class FeatureFlaggingSystem {
1214
private static volatile ConfigurationSourceService CONFIG_SERVICE;
1315
private static volatile ExposureWriter EXPOSURE_WRITER;
1416
private static volatile SpanEnrichmentWriter SPAN_ENRICHMENT_WRITER;
17+
private static volatile FeatureFlaggingGateway.ActivationListener ACTIVATION_LISTENER;
18+
private static volatile boolean STARTED;
1519

1620
private FeatureFlaggingSystem() {}
1721

1822
public static synchronized void start(final SharedCommunicationObjects sco) {
19-
if (CONFIG_SERVICE != null || EXPOSURE_WRITER != null) {
23+
if (STARTED) {
2024
LOGGER.debug("Feature Flagging system already started");
2125
return;
2226
}
2327
LOGGER.debug("Feature Flagging system starting");
2428
final Config config = Config.get();
29+
final FeatureFlaggingConfigurationSource source =
30+
FeatureFlaggingConfigurationSource.fromResolved(
31+
config.getFeatureFlaggingConfigurationSource());
32+
STARTED = true;
33+
34+
if (source == FeatureFlaggingConfigurationSource.OFFLINE) {
35+
LOGGER.debug("Feature Flagging system disabled");
36+
return;
37+
}
38+
if (source == FeatureFlaggingConfigurationSource.AGENTLESS) {
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);
2673
final ExposureWriter exposureWriter = new ExposureWriterImpl(sco, config);
2774
initialize(configService, exposureWriter);
@@ -60,16 +107,17 @@ static void initialize(
60107

61108
static ConfigurationSourceService createConfigurationSourceService(
62109
final SharedCommunicationObjects sco, final Config config) {
63-
final ConfigurationSource configurationSource =
64-
ConfigurationSource.from(config.getFeatureFlaggingConfigurationSource());
110+
final FeatureFlaggingConfigurationSource configurationSource =
111+
FeatureFlaggingConfigurationSource.fromResolved(
112+
config.getFeatureFlaggingConfigurationSource());
65113

66-
if (configurationSource == ConfigurationSource.REMOTE_CONFIG) {
114+
if (configurationSource == FeatureFlaggingConfigurationSource.REMOTE_CONFIG) {
67115
if (!config.isRemoteConfigEnabled()) {
68116
throw new IllegalStateException("Feature Flagging system started without RC");
69117
}
70118
return new RemoteConfigServiceImpl(sco, config);
71119
}
72-
if (configurationSource == ConfigurationSource.AGENTLESS) {
120+
if (configurationSource == FeatureFlaggingConfigurationSource.AGENTLESS) {
73121
return new AgentlessConfigurationSource(config);
74122
}
75123
LOGGER.debug(
@@ -78,12 +126,18 @@ static ConfigurationSourceService createConfigurationSourceService(
78126
}
79127

80128
public static synchronized void stop() {
129+
final FeatureFlaggingGateway.ActivationListener activationListener = ACTIVATION_LISTENER;
81130
final SpanEnrichmentWriter spanEnrichmentWriter = SPAN_ENRICHMENT_WRITER;
82131
final ExposureWriter exposureWriter = EXPOSURE_WRITER;
83132
final ConfigurationSourceService configService = CONFIG_SERVICE;
133+
STARTED = false;
134+
ACTIVATION_LISTENER = null;
84135
SPAN_ENRICHMENT_WRITER = null;
85136
EXPOSURE_WRITER = null;
86137
CONFIG_SERVICE = null;
138+
if (activationListener != null) {
139+
FeatureFlaggingGateway.removeActivationListener(activationListener);
140+
}
87141
try {
88142
if (spanEnrichmentWriter != null) {
89143
spanEnrichmentWriter.close();
@@ -102,25 +156,7 @@ public static synchronized void stop() {
102156
LOGGER.debug("Feature Flagging system stopped");
103157
}
104158

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-
}
159+
static boolean isAwaitingApplicationActivation() {
160+
return ACTIVATION_LISTENER != null;
125161
}
126162
}

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import static datadog.trace.api.config.RemoteConfigConfig.REMOTE_CONFIGURATION_ENABLED;
44
import static datadog.trace.api.featureflag.config.FeatureFlaggingConfig.FEATURE_FLAGS_CONFIGURATION_SOURCE;
55
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
6+
import static org.junit.jupiter.api.Assertions.assertFalse;
67
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
78
import static org.junit.jupiter.api.Assertions.assertNull;
89
import static org.junit.jupiter.api.Assertions.assertThrows;
10+
import static org.junit.jupiter.api.Assertions.assertTrue;
911
import static org.mockito.ArgumentMatchers.any;
1012
import static org.mockito.ArgumentMatchers.eq;
1113
import static org.mockito.Mockito.doThrow;
@@ -27,6 +29,20 @@
2729

2830
class FeatureFlaggingSystemTest {
2931

32+
@Test
33+
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "agentless")
34+
void agentlessStartWaitsForApplicationProviderActivation() {
35+
try {
36+
FeatureFlaggingSystem.start(sharedCommunicationObjects());
37+
38+
assertTrue(FeatureFlaggingSystem.isAwaitingApplicationActivation());
39+
} finally {
40+
FeatureFlaggingSystem.stop();
41+
}
42+
43+
assertFalse(FeatureFlaggingSystem.isAwaitingApplicationActivation());
44+
}
45+
3046
@Test
3147
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "remote_config")
3248
@WithConfig(key = REMOTE_CONFIGURATION_ENABLED, value = "true")
@@ -97,9 +113,8 @@ void explicitRemoteConfigUsesRemoteConfigService() {
97113

98114
@Test
99115
@WithConfig(key = FEATURE_FLAGS_CONFIGURATION_SOURCE, value = "invalid")
100-
void invalidConfigurationSourceUsesAgentlessDefault() {
101-
assertInstanceOf(
102-
AgentlessConfigurationSource.class,
116+
void invalidConfigurationSourceFailsClosed() {
117+
assertNull(
103118
FeatureFlaggingSystem.createConfigurationSourceService(
104119
sharedCommunicationObjects(), Config.get()));
105120
}

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;
@@ -145,6 +146,22 @@ public void testInitializeTimesOutWithoutConfig() throws Exception {
145146
}
146147
}
147148

149+
@Test
150+
public void testInitializeSignalsApplicationProviderActivation() throws Exception {
151+
final FeatureFlaggingGateway.ActivationListener listener =
152+
mock(FeatureFlaggingGateway.ActivationListener.class);
153+
final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class));
154+
FeatureFlaggingGateway.addActivationListener(listener);
155+
try {
156+
evaluator.initialize(1, MILLISECONDS, mock(EvaluationContext.class));
157+
158+
verify(listener).activate();
159+
} finally {
160+
evaluator.shutdown();
161+
FeatureFlaggingGateway.removeActivationListener(listener);
162+
}
163+
}
164+
148165
@Test
149166
public void testInitializeWaitsForNonNullConfig() throws Exception {
150167
final DDEvaluator evaluator = new DDEvaluator(mock(Runnable.class));

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)