Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -125,7 +125,7 @@ public <T> ProviderEvaluation<T> 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) {
Expand Down Expand Up @@ -208,14 +208,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)) {
static boolean isAllocationActive(final Allocation allocation, final Instant now) {
final Instant startDate = allocation.startAtInstant();
if (startDate != null && now.isBefore(startDate)) {
return false;
}

final Date endDate = allocation.endAt;
if (endDate != null && now.after(endDate)) {
final Instant endDate = allocation.endAtInstant();
if (endDate != null && now.isAfter(endDate)) {
return false;
}

Expand Down Expand Up @@ -546,7 +546,9 @@ static AbstractMap<String, Object> flattenContext(final EvaluationContext contex
new FlattenEntry(entry.key + "." + property, structure.getValue(property)));
}
} else {
result.put(entry.key, context.convertValue(value));
result.put(
entry.key,
value.isInstant() ? value.asInstant().toString() : context.convertValue(value));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
import datadog.trace.api.featureflag.ufc.v1.Allocation;
import datadog.trace.api.featureflag.ufc.v1.Flag;
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
import dev.openfeature.sdk.ErrorCode;
Expand All @@ -37,7 +38,8 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.OffsetDateTime;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
Expand Down Expand Up @@ -123,7 +125,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),
};
}

Expand Down Expand Up @@ -212,6 +214,33 @@ public void testNoAllocations() {
assertThat(details.getErrorCode(), nullValue());
}

@Test
public void testAllocationDateAbiAndInstantAccessors() throws Exception {
final Date startAt = Date.from(Instant.parse("2024-01-01T00:00:00Z"));
final Date endAt = Date.from(Instant.parse("2024-12-31T23:59:59Z"));
final Allocation allocation =
new Allocation("allocation", emptyList(), startAt, endAt, emptyList(), true);

assertThat(Allocation.class.getField("startAt").getType(), equalTo(Date.class));
assertThat(Allocation.class.getField("endAt").getType(), equalTo(Date.class));
assertThat(allocation.startAtInstant(), equalTo(startAt.toInstant()));
assertThat(allocation.endAtInstant(), equalTo(endAt.toInstant()));
}

@Test
public void testAllocationWindowHonorsMicrosecondPrecision() {
final Instant startAt = Instant.parse("2024-01-01T00:00:00.123456Z");
final Instant endAt = Instant.parse("2024-01-01T00:00:00.987654Z");
final Allocation allocation =
Allocation.fromInstants("allocation", emptyList(), startAt, endAt, emptyList(), true);

assertThat(
DDEvaluator.isAllocationActive(allocation, startAt.minusNanos(1_000)), equalTo(false));
assertThat(DDEvaluator.isAllocationActive(allocation, startAt), equalTo(true));
assertThat(DDEvaluator.isAllocationActive(allocation, endAt), equalTo(true));
assertThat(DDEvaluator.isAllocationActive(allocation, endAt.plusNanos(1_000)), equalTo(false));
}

private static Arguments[] flatteningTestCases() {
final List<Arguments> arguments = new ArrayList<>();
arguments.add(Arguments.of(emptyMap(), emptyMap()));
Expand All @@ -227,6 +256,8 @@ private static Arguments[] flatteningTestCases() {
Arguments.of(
mapOf("map", mapOf("key1", 1, "key2", 2, "key3", mapOf("key4", 4))),
mapOf("map.key1", 1, "map.key2", 2, "map.key3.key4", 4)));
final Instant instant = Instant.parse("2026-07-10T12:34:56Z");
arguments.add(Arguments.of(mapOf("instant", instant), mapOf("instant", instant.toString())));
return arguments.toArray(new Arguments[0]);
}

Expand Down Expand Up @@ -410,7 +441,8 @@ public Date fromJson(final JsonReader reader) throws IOException {
return reader.nextNull();
}
try {
return Date.from(OffsetDateTime.parse(reader.nextString()).toInstant());
return Date.from(
DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(reader.nextString(), Instant::from));
} catch (final Exception ignored) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package datadog.trace.api.featureflag.ufc.v1;

import java.time.Instant;
import java.util.Date;
import java.util.List;

Expand All @@ -11,18 +12,69 @@ public class Allocation {
public final List<Split> splits;
public final Boolean doLog;

private final transient Instant preciseStartAt;
private final transient Instant preciseEndAt;

public Allocation(
final String key,
final List<Rule> rules,
final Date startAt,
final Date endAt,
final List<Split> splits,
final Boolean doLog) {
this(
key,
rules,
startAt,
endAt,
splits,
doLog,
startAt == null ? null : startAt.toInstant(),
endAt == null ? null : endAt.toInstant());
Comment on lines +32 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Preserve mutable Date constructor behavior

Programmatically constructed allocations whose Date window is adjusted after construction can activate or expire at the stale original time.

Assertion details
  • Input: Construct an Allocation with startAt at 12:00, mutate that same Date to 13:00, then evaluate at 12:30.
  • Expected: Allocations created through the retained Date constructor should preserve the prior behavior of reading the public mutable Date fields, while fromInstants continues preserving nanosecond precision.
  • Actual: The constructor stores the original Date-derived Instants in preciseStartAt/preciseEndAt. Mutating either supplied Date later leaves isAllocationActive evaluating the stale construction-time window.
Suggested change
startAt == null ? null : startAt.toInstant(),
endAt == null ? null : endAt.toInstant());
null,
null);

Was this helpful? React 馃憤 or 馃憥
馃 Datadog Autotest 路 What is Autotest?@DataDog review to ask questions 路 Any feedback? Reach out in #autotest

}

private Allocation(
final String key,
final List<Rule> rules,
final Date startAt,
final Date endAt,
final List<Split> splits,
final Boolean doLog,
final Instant preciseStartAt,
final Instant preciseEndAt) {
this.key = key;
this.rules = rules;
this.startAt = startAt;
this.endAt = endAt;
this.splits = splits;
this.doLog = doLog;
this.preciseStartAt = preciseStartAt;
this.preciseEndAt = preciseEndAt;
}

public static Allocation fromInstants(
final String key,
final List<Rule> rules,
final Instant startAt,
final Instant endAt,
final List<Split> splits,
final Boolean doLog) {
return new Allocation(
key,
rules,
startAt == null ? null : Date.from(startAt),
endAt == null ? null : Date.from(endAt),
splits,
doLog,
startAt,
endAt);
}

public Instant startAtInstant() {
return preciseStartAt != null ? preciseStartAt : startAt == null ? null : startAt.toInstant();
}

public Instant endAtInstant() {
return preciseEndAt != null ? preciseEndAt : endAt == null ? null : endAt.toInstant();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.datadog.featureflag;

import static datadog.trace.api.telemetry.LogCollector.EXCLUDE_TELEMETRY;
import static datadog.trace.util.AgentThreadFactory.AgentThread.FEATURE_FLAG_EXPOSURE_PROCESSOR;
import static datadog.trace.util.AgentThreadFactory.newAgentThread;
import static java.util.concurrent.TimeUnit.SECONDS;
Expand Down Expand Up @@ -178,15 +179,22 @@ protected void flushIfNecessary() {
return;
}
if (shouldFlush()) {
final String requestBodyJson;
try {
final ExposuresRequest exposures = new ExposuresRequest(this.context, this.buffer);
final String reqBod = jsonAdapter.toJson(exposures);
requestBodyJson = jsonAdapter.toJson(exposures);
} catch (RuntimeException e) {
LOGGER.error(EXCLUDE_TELEMETRY, "Could not serialize exposures; dropping batch", e);
this.buffer.clear();
return;
}
try {
final RequestBody requestBody =
RequestBody.create(okhttp3.MediaType.parse("application/json"), reqBod);
RequestBody.create(okhttp3.MediaType.parse("application/json"), requestBodyJson);
evp.post("exposures", requestBody, stream -> null, null, false);
this.buffer.clear();
} catch (Exception e) {
LOGGER.error("Could not submit exposures", e);
LOGGER.debug("Could not submit exposures", e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,40 @@
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import datadog.remoteconfig.ConfigurationDeserializer;
import datadog.trace.api.featureflag.ufc.v1.Allocation;
import datadog.trace.api.featureflag.ufc.v1.Flag;
import datadog.trace.api.featureflag.ufc.v1.Rule;
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
import datadog.trace.api.featureflag.ufc.v1.Split;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
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.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import okio.BufferedSource;
import okio.Okio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class UniversalFlagConfigParser implements ConfigurationDeserializer<ServerConfiguration> {

private static final Logger LOGGER = LoggerFactory.getLogger(UniversalFlagConfigParser.class);

static final UniversalFlagConfigParser INSTANCE = new UniversalFlagConfigParser();

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(AllocationAdapter.FACTORY)
.add(FlagMapAdapter.FACTORY)
.build();
private static final JsonAdapter<ServerConfiguration> V1_ADAPTER =
MOSHI.adapter(ServerConfiguration.class);

Expand Down Expand Up @@ -97,8 +108,11 @@ public Map<String, Flag> 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();
Expand All @@ -112,28 +126,86 @@ public void toJson(@Nonnull final JsonWriter writer, @Nullable final Map<String,
}
}

static final class DateAdapter extends JsonAdapter<Date> {
static final class InstantAdapter extends JsonAdapter<Instant> {

@Nullable
@Override
public Date fromJson(@Nonnull final JsonReader reader) throws IOException {
final String date = reader.nextString();
if (date == null) {
return null;
public Instant fromJson(@Nonnull final JsonReader reader) throws IOException {
if (reader.peek() == JsonReader.Token.NULL) {
return reader.nextNull();
}
try {
final Instant instant = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date, Instant::from);
return Date.from(instant);
} catch (Exception e) {
// ignore wrongly set dates
return parseInstant(reader.nextString());
}

@Override
public void toJson(@Nonnull final JsonWriter writer, @Nullable final Instant value)
throws IOException {
throw new UnsupportedOperationException("Reading only adapter");
}
}

static final class AllocationAdapter extends JsonAdapter<Allocation> {

static final Factory FACTORY =
new Factory() {
@Nullable
@Override
public JsonAdapter<?> create(
@Nonnull final Type type,
@Nonnull final Set<? extends Annotation> annotations,
@Nonnull final Moshi moshi) {
if (!annotations.isEmpty() || !Types.equals(type, Allocation.class)) {
return null;
}
return new AllocationAdapter(moshi.adapter(AllocationJson.class));
}
};

private final JsonAdapter<AllocationJson> delegate;

AllocationAdapter(final JsonAdapter<AllocationJson> delegate) {
this.delegate = delegate;
}

@Nullable
@Override
public Allocation fromJson(@Nonnull final JsonReader reader) throws IOException {
final AllocationJson allocation = delegate.fromJson(reader);
if (allocation == null) {
return null;
}
return Allocation.fromInstants(
allocation.key,
allocation.rules,
allocation.startAt,
allocation.endAt,
allocation.splits,
allocation.doLog);
}

@Override
public void toJson(@Nonnull final JsonWriter writer, @Nullable final Date value)
public void toJson(@Nonnull final JsonWriter writer, @Nullable final Allocation value)
throws IOException {
throw new UnsupportedOperationException("Reading only adapter");
}
}

static final class AllocationJson {
String key;
List<Rule> rules;
Instant startAt;
Instant endAt;
List<Split> splits;
Boolean doLog;
}

@Nullable
private static Instant parseInstant(final String date) {
try {
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date, Instant::from);
} catch (Exception e) {
// ignore wrongly set dates
return null;
}
}
}
Loading
Loading