Skip to content

Commit cfaca97

Browse files
committed
fix(feature-flags): align agentless HTTP behavior
1 parent bd0e347 commit cfaca97

2 files changed

Lines changed: 312 additions & 165 deletions

File tree

products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java

Lines changed: 149 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package com.datadog.featureflag;
22

33
import static datadog.communication.http.OkHttpUtils.prepareRequest;
4+
import static datadog.communication.http.OkHttpUtils.sendWithRetries;
45
import static datadog.trace.util.AgentThreadFactory.AgentThread.FEATURE_FLAG_CONFIGURATION_POLLER;
56
import static datadog.trace.util.Strings.isBlank;
67

8+
import datadog.communication.http.HttpRetryPolicy;
79
import datadog.communication.http.OkHttpUtils;
810
import datadog.logging.RatelimitedLogger;
911
import datadog.trace.api.Config;
1012
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
1113
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
1214
import datadog.trace.util.AgentThreadFactory;
1315
import java.io.IOException;
16+
import java.io.InterruptedIOException;
1417
import java.net.HttpURLConnection;
1518
import java.util.HashMap;
1619
import java.util.Map;
@@ -50,8 +53,6 @@ final class AgentlessConfigurationSource implements ConfigurationSourceService {
5053
private final long pollIntervalMillis;
5154
private final UfcHttpClient client;
5255
private final ScheduledExecutorService executor;
53-
private final RetrySleeper retrySleeper;
54-
private final DoubleSupplier jitter;
5556
private final RatelimitedLogger ratelimitedLogger;
5657
private final Object lifecycleLock = new Object();
5758
private final AtomicBoolean polling = new AtomicBoolean();
@@ -71,11 +72,12 @@ private AgentlessConfigurationSource(final Config config, final HttpUrl endpoint
7172
new OkHttpUfcHttpClient(
7273
OkHttpUtils.buildHttpClient(
7374
endpoint,
74-
millis(config.getFeatureFlaggingConfigurationSourceRequestTimeoutSeconds()))),
75+
millis(config.getFeatureFlaggingConfigurationSourceRequestTimeoutSeconds())),
76+
millis(config.getFeatureFlaggingConfigurationSourcePollIntervalSeconds()),
77+
TimeUnit.MILLISECONDS::sleep,
78+
() -> ThreadLocalRandom.current().nextDouble(1 - RETRY_JITTER, 1 + RETRY_JITTER)),
7579
Executors.newSingleThreadScheduledExecutor(
7680
new AgentThreadFactory(FEATURE_FLAG_CONFIGURATION_POLLER)),
77-
TimeUnit.MILLISECONDS::sleep,
78-
() -> ThreadLocalRandom.current().nextDouble(1 - RETRY_JITTER, 1 + RETRY_JITTER),
7981
new RatelimitedLogger(LOGGER, MINUTES_BETWEEN_WARNINGS, TimeUnit.MINUTES));
8082
}
8183

@@ -91,8 +93,6 @@ private AgentlessConfigurationSource(final Config config, final HttpUrl endpoint
9193
pollIntervalMillis,
9294
client,
9395
executor,
94-
TimeUnit.MILLISECONDS::sleep,
95-
() -> 1.0,
9696
new RatelimitedLogger(LOGGER, MINUTES_BETWEEN_WARNINGS, TimeUnit.MINUTES));
9797
}
9898

@@ -102,35 +102,12 @@ private AgentlessConfigurationSource(final Config config, final HttpUrl endpoint
102102
final long pollIntervalMillis,
103103
final UfcHttpClient client,
104104
final ScheduledExecutorService executor,
105-
final RetrySleeper retrySleeper,
106-
final DoubleSupplier jitter) {
107-
this(
108-
endpoint,
109-
config,
110-
pollIntervalMillis,
111-
client,
112-
executor,
113-
retrySleeper,
114-
jitter,
115-
new RatelimitedLogger(LOGGER, MINUTES_BETWEEN_WARNINGS, TimeUnit.MINUTES));
116-
}
117-
118-
AgentlessConfigurationSource(
119-
final HttpUrl endpoint,
120-
final Config config,
121-
final long pollIntervalMillis,
122-
final UfcHttpClient client,
123-
final ScheduledExecutorService executor,
124-
final RetrySleeper retrySleeper,
125-
final DoubleSupplier jitter,
126105
final RatelimitedLogger ratelimitedLogger) {
127106
this.endpoint = endpoint;
128107
this.config = config;
129108
this.pollIntervalMillis = pollIntervalMillis;
130109
this.client = client;
131110
this.executor = executor;
132-
this.retrySleeper = retrySleeper;
133-
this.jitter = jitter;
134111
this.ratelimitedLogger = ratelimitedLogger;
135112
}
136113

@@ -184,55 +161,28 @@ private void pollOnceSafely() {
184161
}
185162

186163
private boolean fetchAndApply() {
187-
for (int attempt = 1; ; attempt++) {
188-
try {
189-
final UfcHttpResponse response = client.fetch(endpoint, config, etag);
190-
if (closed) {
191-
return false;
192-
}
193-
if (isRetryableStatus(response.status)) {
194-
if (attempt < MAX_ATTEMPTS) {
195-
if (!waitBeforeRetry(attempt)) {
196-
return false;
197-
}
198-
continue;
199-
}
200-
ratelimitedLogger.warn(
201-
"Feature Flagging agentless endpoint failed after {} attempts with HTTP {}",
202-
MAX_ATTEMPTS,
203-
response.status);
204-
return false;
205-
}
206-
synchronized (lifecycleLock) {
207-
return !closed && apply(response);
208-
}
209-
} catch (final IOException e) {
210-
if (closed) {
211-
return false;
212-
}
213-
if (attempt == MAX_ATTEMPTS) {
214-
ratelimitedLogger.warn(
215-
"Feature Flagging agentless endpoint request failed after {} attempts",
216-
MAX_ATTEMPTS,
217-
e);
218-
return false;
219-
}
220-
if (!waitBeforeRetry(attempt)) {
221-
return false;
222-
}
223-
}
224-
}
225-
}
226-
227-
private boolean waitBeforeRetry(final int attempt) {
228-
if (closed) {
229-
return false;
230-
}
231164
try {
232-
retrySleeper.sleep(retryDelayMillis(pollIntervalMillis, attempt, jitter.getAsDouble()));
233-
return !closed;
234-
} catch (final InterruptedException e) {
235-
Thread.currentThread().interrupt();
165+
final UfcHttpResponse response = client.fetch(endpoint, config, etag);
166+
if (closed) {
167+
return false;
168+
}
169+
if (isRetryableStatus(response.status)) {
170+
ratelimitedLogger.warn(
171+
"Feature Flagging agentless endpoint failed after {} attempts with HTTP {}",
172+
MAX_ATTEMPTS,
173+
response.status);
174+
return false;
175+
}
176+
synchronized (lifecycleLock) {
177+
return !closed && apply(response);
178+
}
179+
} catch (final IOException e) {
180+
if (!closed) {
181+
ratelimitedLogger.warn(
182+
"Feature Flagging agentless endpoint request failed after {} attempts",
183+
MAX_ATTEMPTS,
184+
e);
185+
}
236186
return false;
237187
}
238188
}
@@ -244,7 +194,7 @@ private boolean apply(final UfcHttpResponse response) {
244194
if (response.status == HttpURLConnection.HTTP_UNAUTHORIZED
245195
|| response.status == HttpURLConnection.HTTP_FORBIDDEN) {
246196
ratelimitedLogger.warn(
247-
"Feature Flagging agentless endpoint returned HTTP {}; verify DD_API_KEY is configured and valid",
197+
"Feature Flagging agentless endpoint returned HTTP {}; verify endpoint authentication",
248198
response.status);
249199
return false;
250200
}
@@ -356,11 +306,30 @@ static final class UfcHttpResponse {
356306

357307
static final class OkHttpUfcHttpClient implements UfcHttpClient {
358308
private final OkHttpClient httpClient;
359-
private final AtomicReference<Call> activeCall = new AtomicReference<>();
309+
private final long pollIntervalMillis;
310+
private final RetrySleeper retrySleeper;
311+
private final DoubleSupplier jitter;
312+
private final AtomicBoolean fetching = new AtomicBoolean();
360313
private final AtomicBoolean cancelled = new AtomicBoolean();
314+
private final AtomicReference<Call> activeCall = new AtomicReference<>();
361315

362316
OkHttpUfcHttpClient(final OkHttpClient httpClient) {
317+
this(
318+
httpClient,
319+
TimeUnit.SECONDS.toMillis(30),
320+
TimeUnit.MILLISECONDS::sleep,
321+
() -> ThreadLocalRandom.current().nextDouble(1 - RETRY_JITTER, 1 + RETRY_JITTER));
322+
}
323+
324+
OkHttpUfcHttpClient(
325+
final OkHttpClient httpClient,
326+
final long pollIntervalMillis,
327+
final RetrySleeper retrySleeper,
328+
final DoubleSupplier jitter) {
363329
this.httpClient = httpClient;
330+
this.pollIntervalMillis = pollIntervalMillis;
331+
this.retrySleeper = retrySleeper;
332+
this.jitter = jitter;
364333
}
365334

366335
@Override
@@ -371,22 +340,50 @@ public UfcHttpResponse fetch(final HttpUrl endpoint, final Config config, final
371340
headers.put("If-None-Match", etag);
372341
}
373342
// Leave Accept-Encoding unset so OkHttp negotiates gzip and transparently decompresses it.
374-
final Request request = prepareRequest(endpoint, headers, config, true).get().build();
375-
final Call call = httpClient.newCall(request);
376-
if (!activeCall.compareAndSet(null, call)) {
343+
final Request request =
344+
prepareRequest(endpoint, headers, config, isDatadogManagedEndpoint(endpoint, config))
345+
.get()
346+
.build();
347+
if (!fetching.compareAndSet(false, true)) {
377348
throw new IllegalStateException("Feature Flagging HTTP request already in flight");
378349
}
379350
if (cancelled.get()) {
380-
call.cancel();
351+
fetching.set(false);
352+
throw new InterruptedIOException("Feature Flagging HTTP client is closed");
381353
}
382354
try {
383-
final Response response = call.execute();
384-
try (ResponseBody responseBody = response.body()) {
385-
final byte[] body = responseBody != null ? responseBody.bytes() : null;
386-
return new UfcHttpResponse(response.code(), response.header("ETag"), body);
387-
}
355+
final HttpRetryPolicy.Factory retryPolicyFactory =
356+
new HttpRetryPolicy.Factory(0, 0, 0) {
357+
@Override
358+
public HttpRetryPolicy create() {
359+
return new AgentlessRetryPolicy(
360+
cancelled, pollIntervalMillis, retrySleeper, jitter);
361+
}
362+
};
363+
final Call.Factory callFactory =
364+
retryRequest -> {
365+
final Call call = httpClient.newCall(retryRequest);
366+
activeCall.set(call);
367+
if (cancelled.get()) {
368+
call.cancel();
369+
}
370+
return call;
371+
};
372+
return sendWithRetries(
373+
callFactory,
374+
retryPolicyFactory,
375+
request,
376+
response -> {
377+
final int status = response.code();
378+
final String responseEtag = response.header("ETag");
379+
try (ResponseBody responseBody = response.body()) {
380+
final byte[] body = responseBody != null ? responseBody.bytes() : null;
381+
return new UfcHttpResponse(status, responseEtag, body);
382+
}
383+
});
388384
} finally {
389-
activeCall.compareAndSet(call, null);
385+
activeCall.set(null);
386+
fetching.set(false);
390387
}
391388
}
392389

@@ -398,5 +395,67 @@ public void cancel() {
398395
call.cancel();
399396
}
400397
}
398+
399+
private static boolean isDatadogManagedEndpoint(final HttpUrl endpoint, final Config config) {
400+
return config.getFeatureFlaggingConfigurationSourceAgentlessBaseUrl() == null
401+
&& endpoint.isHttps()
402+
&& endpoint.host().equalsIgnoreCase("ufc-server.ff-cdn." + config.getSite());
403+
}
404+
}
405+
406+
static final class AgentlessRetryPolicy extends HttpRetryPolicy {
407+
private final AtomicBoolean cancelled;
408+
private final long pollIntervalMillis;
409+
private final RetrySleeper retrySleeper;
410+
private final DoubleSupplier jitter;
411+
private int retriesLeft = MAX_ATTEMPTS - 1;
412+
private int retryAttempt;
413+
414+
AgentlessRetryPolicy(
415+
final AtomicBoolean cancelled,
416+
final long pollIntervalMillis,
417+
final RetrySleeper retrySleeper,
418+
final DoubleSupplier jitter) {
419+
super(0, 0, 0, false);
420+
this.cancelled = cancelled;
421+
this.pollIntervalMillis = pollIntervalMillis;
422+
this.retrySleeper = retrySleeper;
423+
this.jitter = jitter;
424+
}
425+
426+
@Override
427+
public boolean shouldRetry(final Exception exception) {
428+
return exception instanceof IOException
429+
&& !Thread.currentThread().isInterrupted()
430+
&& reserveRetry();
431+
}
432+
433+
@Override
434+
public boolean shouldRetry(@Nullable final Response response) {
435+
return response != null && isRetryableStatus(response.code()) && reserveRetry();
436+
}
437+
438+
private boolean reserveRetry() {
439+
if (cancelled.get() || retriesLeft == 0) {
440+
return false;
441+
}
442+
retriesLeft--;
443+
retryAttempt++;
444+
return true;
445+
}
446+
447+
@Override
448+
public void backoff() throws IOException {
449+
if (cancelled.get()) {
450+
throw new InterruptedIOException("Feature Flagging HTTP client is closed");
451+
}
452+
try {
453+
retrySleeper.sleep(
454+
retryDelayMillis(pollIntervalMillis, retryAttempt, jitter.getAsDouble()));
455+
} catch (final InterruptedException e) {
456+
Thread.currentThread().interrupt();
457+
throw new InterruptedIOException("Feature Flagging retry interrupted");
458+
}
459+
}
401460
}
402461
}

0 commit comments

Comments
 (0)