Skip to content

Commit ba90dea

Browse files
committed
Add Datadog-managed agentless UFC polling
1 parent 07f94a5 commit ba90dea

2 files changed

Lines changed: 744 additions & 0 deletions

File tree

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package com.datadog.featureflag;
2+
3+
import static datadog.communication.http.OkHttpUtils.prepareRequest;
4+
5+
import datadog.communication.http.OkHttpUtils;
6+
import datadog.trace.api.Config;
7+
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
8+
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
9+
import java.io.IOException;
10+
import java.net.HttpURLConnection;
11+
import java.net.URLEncoder;
12+
import java.util.HashMap;
13+
import java.util.Locale;
14+
import java.util.Map;
15+
import java.util.concurrent.Executors;
16+
import java.util.concurrent.ScheduledExecutorService;
17+
import java.util.concurrent.ScheduledFuture;
18+
import java.util.concurrent.ThreadFactory;
19+
import java.util.concurrent.TimeUnit;
20+
import java.util.concurrent.atomic.AtomicBoolean;
21+
import okhttp3.HttpUrl;
22+
import okhttp3.OkHttpClient;
23+
import okhttp3.Request;
24+
import okhttp3.Response;
25+
import okhttp3.ResponseBody;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
29+
final class AgentlessConfigurationSource implements ConfigurationSourceService {
30+
private static final Logger LOGGER = LoggerFactory.getLogger(AgentlessConfigurationSource.class);
31+
32+
// TODO before merge: confirm the final backend route with the server-distribution API owners.
33+
private static final String DATADOG_API_SERVER_DISTRIBUTION_PATH =
34+
"/api/v2/feature-flagging/config/server-distribution";
35+
private static final int MAX_ATTEMPTS = 3;
36+
37+
private final HttpUrl endpoint;
38+
private final Config config;
39+
private final long pollIntervalMillis;
40+
private final UfcHttpClient client;
41+
private final ScheduledExecutorService executor;
42+
private final AtomicBoolean polling = new AtomicBoolean();
43+
private volatile boolean closed;
44+
private volatile ScheduledFuture<?> scheduledPoll;
45+
private volatile String etag;
46+
47+
AgentlessConfigurationSource(final Config config) {
48+
this(config, endpoint(config));
49+
}
50+
51+
private AgentlessConfigurationSource(final Config config, final HttpUrl endpoint) {
52+
this(
53+
endpoint,
54+
config,
55+
millis(config.getFeatureFlaggingConfigurationSourcePollIntervalSeconds()),
56+
new OkHttpUfcHttpClient(
57+
OkHttpUtils.buildHttpClient(
58+
endpoint,
59+
millis(config.getFeatureFlaggingConfigurationSourceRequestTimeoutSeconds()))),
60+
Executors.newSingleThreadScheduledExecutor(new UfcHttpThreadFactory()));
61+
}
62+
63+
AgentlessConfigurationSource(
64+
final HttpUrl endpoint,
65+
final Config config,
66+
final long pollIntervalMillis,
67+
final UfcHttpClient client,
68+
final ScheduledExecutorService executor) {
69+
this.endpoint = endpoint;
70+
this.config = config;
71+
this.pollIntervalMillis = pollIntervalMillis;
72+
this.client = client;
73+
this.executor = executor;
74+
}
75+
76+
@Override
77+
public void init() {
78+
if (closed) {
79+
return;
80+
}
81+
scheduledPoll =
82+
executor.scheduleWithFixedDelay(
83+
this::pollOnceSafely, 0, pollIntervalMillis, TimeUnit.MILLISECONDS);
84+
}
85+
86+
boolean pollOnce() {
87+
if (closed || !polling.compareAndSet(false, true)) {
88+
return false;
89+
}
90+
try {
91+
return fetchAndApply();
92+
} finally {
93+
polling.set(false);
94+
}
95+
}
96+
97+
@Override
98+
public void close() {
99+
closed = true;
100+
if (scheduledPoll != null) {
101+
scheduledPoll.cancel(true);
102+
scheduledPoll = null;
103+
}
104+
executor.shutdownNow();
105+
}
106+
107+
private void pollOnceSafely() {
108+
try {
109+
pollOnce();
110+
} catch (final RuntimeException e) {
111+
LOGGER.debug("Unexpected error while polling Feature Flagging HTTP configuration source", e);
112+
}
113+
}
114+
115+
private boolean fetchAndApply() {
116+
for (int attempt = 1; ; attempt++) {
117+
try {
118+
final UfcHttpResponse response = client.fetch(endpoint, config, etag);
119+
if (isRetryableStatus(response.status) && attempt < MAX_ATTEMPTS) {
120+
continue;
121+
}
122+
return apply(response);
123+
} catch (final IOException e) {
124+
if (attempt == MAX_ATTEMPTS) {
125+
LOGGER.debug("Feature Flagging HTTP configuration source request failed", e);
126+
return false;
127+
}
128+
}
129+
}
130+
}
131+
132+
private boolean apply(final UfcHttpResponse response) {
133+
if (response.status == HttpURLConnection.HTTP_NOT_MODIFIED) {
134+
updateEtag(response.etag);
135+
return true;
136+
}
137+
if (response.status == HttpURLConnection.HTTP_UNAUTHORIZED
138+
|| response.status == HttpURLConnection.HTTP_FORBIDDEN
139+
|| response.status != HttpURLConnection.HTTP_OK
140+
|| response.body == null) {
141+
return false;
142+
}
143+
final ServerConfiguration configuration;
144+
try {
145+
configuration =
146+
RemoteConfigServiceImpl.UniversalFlagConfigDeserializer.INSTANCE.deserialize(
147+
response.body);
148+
} catch (final IOException | RuntimeException e) {
149+
LOGGER.debug("Feature Flagging HTTP configuration source returned malformed UFC payload", e);
150+
return false;
151+
}
152+
if (configuration == null) {
153+
return false;
154+
}
155+
updateEtag(response.etag);
156+
FeatureFlaggingGateway.dispatch(configuration);
157+
return true;
158+
}
159+
160+
private static boolean isRetryableStatus(final int status) {
161+
return status == HttpURLConnection.HTTP_CLIENT_TIMEOUT
162+
|| status == 429
163+
|| (status >= 500 && status <= 599);
164+
}
165+
166+
private void updateEtag(final String nextEtag) {
167+
if (nextEtag != null && !nextEtag.trim().isEmpty()) {
168+
etag = nextEtag;
169+
}
170+
}
171+
172+
static HttpUrl endpoint(final Config config) {
173+
final String endpoint = datadogApiServerDistributionEndpoint(config);
174+
final HttpUrl parsed = HttpUrl.parse(endpoint);
175+
if (parsed == null) {
176+
throw new IllegalArgumentException(
177+
"Invalid Feature Flagging HTTP configuration source URL: " + endpoint);
178+
}
179+
return parsed;
180+
}
181+
182+
private static String datadogApiServerDistributionEndpoint(final Config config) {
183+
final StringBuilder endpoint =
184+
new StringBuilder("https://api.")
185+
.append(config.getSite().toLowerCase(Locale.ROOT))
186+
.append(DATADOG_API_SERVER_DISTRIBUTION_PATH);
187+
final String env = config.getEnv();
188+
if (env != null && !env.isEmpty()) {
189+
endpoint.append("?dd_env=").append(urlEncode(env));
190+
}
191+
return endpoint.toString();
192+
}
193+
194+
private static String urlEncode(final String value) {
195+
try {
196+
return URLEncoder.encode(value, "UTF-8");
197+
} catch (final IOException e) {
198+
throw new IllegalArgumentException("Unable to encode Feature Flagging environment", e);
199+
}
200+
}
201+
202+
static long millis(final int seconds) {
203+
return Math.max(1L, seconds * 1000L);
204+
}
205+
206+
interface UfcHttpClient {
207+
UfcHttpResponse fetch(HttpUrl endpoint, Config config, String etag) throws IOException;
208+
}
209+
210+
static final class UfcHttpResponse {
211+
final int status;
212+
final String etag;
213+
final byte[] body;
214+
215+
UfcHttpResponse(final int status, final String etag, final byte[] body) {
216+
this.status = status;
217+
this.etag = etag;
218+
this.body = body;
219+
}
220+
}
221+
222+
static final class OkHttpUfcHttpClient implements UfcHttpClient {
223+
private final OkHttpClient httpClient;
224+
225+
OkHttpUfcHttpClient(final OkHttpClient httpClient) {
226+
this.httpClient = httpClient;
227+
}
228+
229+
@Override
230+
public UfcHttpResponse fetch(final HttpUrl endpoint, final Config config, final String etag)
231+
throws IOException {
232+
final Map<String, String> headers = new HashMap<>();
233+
if (etag != null) {
234+
headers.put("If-None-Match", etag);
235+
}
236+
final Request request = prepareRequest(endpoint, headers, config, true).get().build();
237+
try (Response response = httpClient.newCall(request).execute()) {
238+
final ResponseBody responseBody = response.body();
239+
return new UfcHttpResponse(response.code(), response.header("ETag"), responseBody.bytes());
240+
}
241+
}
242+
}
243+
244+
static final class UfcHttpThreadFactory implements ThreadFactory {
245+
@Override
246+
public Thread newThread(final Runnable runnable) {
247+
final Thread thread = new Thread(runnable, "dd-feature-flagging-http-poller");
248+
thread.setDaemon(true);
249+
return thread;
250+
}
251+
}
252+
}

0 commit comments

Comments
 (0)