diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java index 91c0aafdc7a..fd787413437 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/DDEvaluator.java @@ -26,8 +26,8 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.Instant; import java.util.AbstractMap; -import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; @@ -114,7 +114,7 @@ public ProviderEvaluation evaluate( return error(defaultValue, ErrorCode.GENERAL, "Missing allocations for flag " + key); } - final Date now = new Date(); + final Instant now = Instant.now(); final String targetingKey = context.getTargetingKey(); for (final Allocation allocation : flag.allocations) { @@ -197,14 +197,14 @@ private static boolean isEmpty(final List list) { return list == null || list.isEmpty(); } - private static boolean isAllocationActive(final Allocation allocation, final Date now) { - final Date startDate = allocation.startAt; - if (startDate != null && now.before(startDate)) { + private static boolean isAllocationActive(final Allocation allocation, final Instant now) { + final Instant startDate = allocation.startAt; + if (startDate != null && now.isBefore(startDate)) { return false; } - final Date endDate = allocation.endAt; - if (endDate != null && now.after(endDate)) { + final Instant endDate = allocation.endAt; + if (endDate != null && now.isAfter(endDate)) { return false; } diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java index bb86c409bad..8a1ff05276c 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/DDEvaluatorTest.java @@ -45,15 +45,12 @@ import dev.openfeature.sdk.MutableContext; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.Value; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -129,7 +126,7 @@ private static Arguments[] valueMappingTestCases() { Arguments.of(Value.class, null, null), // Unsupported - Arguments.of(Date.class, "21-12-2023", IllegalArgumentException.class), + Arguments.of(Long.class, 42L, IllegalArgumentException.class), }; } @@ -763,8 +760,8 @@ private Flag createTimeBasedFlag() { new Allocation( "time-alloc", null, - parseDate("2022-01-01T00:00:00Z"), - parseDate("2022-12-31T23:59:59Z"), + parseInstant("2022-01-01T00:00:00Z"), + parseInstant("2022-12-31T23:59:59Z"), splits, false)); @@ -1170,7 +1167,7 @@ private Flag createFutureAllocationFlag() { // Allocation that starts in the future (2050) final Allocation allocation = new Allocation( - "future-alloc", null, parseDate("2050-01-01T00:00:00Z"), null, splits, false); + "future-alloc", null, parseInstant("2050-01-01T00:00:00Z"), null, splits, false); return new Flag( "future-allocation-flag", true, ValueType.STRING, variants, singletonList(allocation)); @@ -1368,13 +1365,7 @@ private static Map mapOf(final Object... props) { return result; } - private static Date parseDate(String dateString) { - try { - SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - formatter.setTimeZone(TimeZone.getTimeZone("UTC")); - return formatter.parse(dateString); - } catch (ParseException e) { - throw new RuntimeException("Failed to parse date: " + dateString, e); - } + private static Instant parseInstant(String value) { + return Instant.parse(value); } } diff --git a/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/ufc/v1/Allocation.java b/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/ufc/v1/Allocation.java index 2ce031243bb..9451f1222f7 100644 --- a/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/ufc/v1/Allocation.java +++ b/products/feature-flagging/feature-flagging-bootstrap/src/main/java/datadog/trace/api/featureflag/ufc/v1/Allocation.java @@ -1,21 +1,21 @@ package datadog.trace.api.featureflag.ufc.v1; -import java.util.Date; +import java.time.Instant; import java.util.List; public class Allocation { public final String key; public final List rules; - public final Date startAt; - public final Date endAt; + public final Instant startAt; + public final Instant endAt; public final List splits; public final Boolean doLog; public Allocation( final String key, final List rules, - final Date startAt, - final Date endAt, + final Instant startAt, + final Instant endAt, final List splits, final Boolean doLog) { this.key = key; diff --git a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java index ec28f684a96..4b9c2d3a6e1 100644 --- a/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java +++ b/products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java @@ -23,17 +23,20 @@ import java.lang.reflect.Type; import java.time.Instant; import java.time.format.DateTimeFormatter; -import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import okio.Okio; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RemoteConfigServiceImpl implements RemoteConfigService, ConfigurationChangesTypedListener { + private static final Logger LOGGER = LoggerFactory.getLogger(RemoteConfigServiceImpl.class); + private final ConfigurationPoller configurationPoller; public RemoteConfigServiceImpl(final SharedCommunicationObjects sco, final Config config) { @@ -69,7 +72,10 @@ static class UniversalFlagConfigDeserializer static final UniversalFlagConfigDeserializer INSTANCE = new UniversalFlagConfigDeserializer(); private static final Moshi MOSHI = - new Moshi.Builder().add(Date.class, new DateAdapter()).add(FlagMapAdapter.FACTORY).build(); + new Moshi.Builder() + .add(Instant.class, new InstantAdapter()) + .add(FlagMapAdapter.FACTORY) + .build(); private static final JsonAdapter V1_ADAPTER = MOSHI.adapter(ServerConfiguration.class); @@ -121,8 +127,11 @@ public Map fromJson(@Nonnull final JsonReader reader) throws IOExc if (flag != null) { flags.put(flagKey, flag); } - } catch (JsonDataException | IllegalArgumentException ignored) { - // A malformed flag must not prevent other flags in the same config from evaluating. + } catch (JsonDataException | IllegalArgumentException error) { + LOGGER.warn( + "Dropping malformed FFE flag {} during remote config deserialization: {}", + flagKey, + error.toString()); } } reader.endObject(); @@ -136,18 +145,17 @@ public void toJson(@Nonnull final JsonWriter writer, @Nullable final Map { + static class InstantAdapter extends JsonAdapter { @Nullable @Override - public Date fromJson(@Nonnull final JsonReader reader) throws IOException { + public Instant fromJson(@Nonnull final JsonReader reader) throws IOException { final String date = reader.nextString(); if (date == null) { return null; } try { - final Instant instant = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date, Instant::from); - return Date.from(instant); + return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date, Instant::from); } catch (Exception e) { // ignore wrongly set dates return null; @@ -155,7 +163,7 @@ public Date fromJson(@Nonnull final JsonReader reader) throws IOException { } @Override - public void toJson(@Nonnull final JsonWriter writer, @Nullable final Date value) + public void toJson(@Nonnull final JsonWriter writer, @Nullable final Instant value) throws IOException { throw new UnsupportedOperationException("Reading only adapter"); } diff --git a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/RemoteConfigServiceImplTest.java b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/RemoteConfigServiceImplTest.java index 4b8eb568cb6..d504223c1a2 100644 --- a/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/RemoteConfigServiceImplTest.java +++ b/products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/RemoteConfigServiceImplTest.java @@ -25,7 +25,6 @@ import datadog.trace.api.featureflag.FeatureFlaggingGateway; import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration; import java.time.Instant; -import java.util.Date; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -174,47 +173,49 @@ void skipsUnknownOperatorFlagAndKeepsValidFlag() throws Exception { } @TableTest({ - "scenario | value | expectedEpochMilli", - "utc second | '2023-01-01T00:00:00Z' | 1672531200000 ", - "utc end of year | '2023-12-31T23:59:59Z' | 1704067199000 ", - "leap day | '2024-02-29T12:00:00Z' | 1709208000000 ", - "millisecond precision | '2023-01-01T00:00:00.000Z' | 1672531200000 ", - "three fractional digits | '2023-06-15T14:30:45.123Z' | 1686839445123 ", - "six fractional digits truncate to millis | '2023-06-15T14:30:45.123456Z' | 1686839445123 ", - "six fractional digits preserve millis | '2023-06-15T14:30:45.235982Z' | 1686839445235 ", - "nine fractional digits truncate to millis | '2023-06-15T14:30:45.123456789Z' | 1686839445123 ", - "one fractional digit | '2023-06-15T14:30:45.1Z' | 1686839445100 ", - "two fractional digits | '2023-06-15T14:30:45.12Z' | 1686839445120 ", - "positive offset | '2023-01-01T01:00:00+01:00' | 1672531200000 ", - "negative offset | '2023-01-01T00:00:00-05:00' | 1672549200000 ", - "date only | '2023-01-01' | ", - "invalid | 'invalid-date' | ", - "empty string | '' | ", - "not a date | 'not-a-date' | ", - "slash date | '2023/01/01T00:00:00Z' | ", - "null | | " + "scenario | value | expectedInstant ", + "utc second | '2023-01-01T00:00:00Z' | '2023-01-01T00:00:00Z' ", + "utc end of year | '2023-12-31T23:59:59Z' | '2023-12-31T23:59:59Z' ", + "leap day | '2024-02-29T12:00:00Z' | '2024-02-29T12:00:00Z' ", + "millisecond precision | '2023-01-01T00:00:00.000Z' | '2023-01-01T00:00:00Z' ", + "three fractional digits | '2023-06-15T14:30:45.123Z' | '2023-06-15T14:30:45.123Z' ", + "six fractional digits | '2023-06-15T14:30:45.123456Z' | '2023-06-15T14:30:45.123456Z' ", + "six fractional digits distinct | '2023-06-15T14:30:45.235982Z' | '2023-06-15T14:30:45.235982Z' ", + "nine fractional digits | '2023-06-15T14:30:45.123456789Z' | '2023-06-15T14:30:45.123456789Z'", + "one fractional digit | '2023-06-15T14:30:45.1Z' | '2023-06-15T14:30:45.100Z' ", + "two fractional digits | '2023-06-15T14:30:45.12Z' | '2023-06-15T14:30:45.120Z' ", + "positive offset | '2023-01-01T01:00:00+01:00' | '2023-01-01T00:00:00Z' ", + "negative offset | '2023-01-01T00:00:00-05:00' | '2023-01-01T05:00:00Z' ", + "date only | '2023-01-01' | ", + "invalid | 'invalid-date' | ", + "empty string | '' | ", + "not a date | 'not-a-date' | ", + "slash date | '2023/01/01T00:00:00Z' | ", + "null | | " }) - void testDateParsing(final String value, final Long expectedEpochMilli) throws Exception { + void testInstantParsing(final String value, final String expectedInstant) throws Exception { final JsonReader reader = mock(JsonReader.class); when(reader.nextString()).thenReturn(value); - final RemoteConfigServiceImpl.DateAdapter adapter = new RemoteConfigServiceImpl.DateAdapter(); + final RemoteConfigServiceImpl.InstantAdapter adapter = + new RemoteConfigServiceImpl.InstantAdapter(); - final Date parsed = adapter.fromJson(reader); - if (expectedEpochMilli == null) { + final Instant parsed = adapter.fromJson(reader); + if (expectedInstant == null) { assertNull(parsed); } else { assertNotNull(parsed); - assertEquals(Instant.ofEpochMilli(expectedEpochMilli), parsed.toInstant()); + assertEquals(Instant.parse(expectedInstant), parsed); } } @Test void testParsingOnlyAdapter() { - final RemoteConfigServiceImpl.DateAdapter adapter = new RemoteConfigServiceImpl.DateAdapter(); + final RemoteConfigServiceImpl.InstantAdapter adapter = + new RemoteConfigServiceImpl.InstantAdapter(); assertThrows( UnsupportedOperationException.class, - () -> adapter.toJson(mock(JsonWriter.class), new Date())); + () -> adapter.toJson(mock(JsonWriter.class), Instant.EPOCH)); } @SuppressWarnings("unchecked")