11package com .datadog .featureflag ;
22
33import static datadog .communication .http .OkHttpUtils .prepareRequest ;
4+ import static datadog .communication .http .OkHttpUtils .sendWithRetries ;
45import static datadog .trace .util .AgentThreadFactory .AgentThread .FEATURE_FLAG_CONFIGURATION_POLLER ;
56import static datadog .trace .util .Strings .isBlank ;
67
8+ import datadog .communication .http .HttpRetryPolicy ;
79import datadog .communication .http .OkHttpUtils ;
810import datadog .logging .RatelimitedLogger ;
911import datadog .trace .api .Config ;
1012import datadog .trace .api .featureflag .FeatureFlaggingGateway ;
1113import datadog .trace .api .featureflag .ufc .v1 .ServerConfiguration ;
1214import datadog .trace .util .AgentThreadFactory ;
1315import java .io .IOException ;
16+ import java .io .InterruptedIOException ;
1417import java .net .HttpURLConnection ;
1518import java .util .HashMap ;
1619import 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