Skip to content

Commit ebc28ca

Browse files
committed
Separate UFC transport parsers
Keep Remote Configuration on raw UFC parsing and give agentless JSON:API its own streaming envelope parser. Share UFC adapters without materializing an intermediate response map.
1 parent 2cee183 commit ebc28ca

6 files changed

Lines changed: 336 additions & 163 deletions

File tree

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,7 @@ private boolean apply(final UfcHttpResponse response) {
253253
}
254254
final ServerConfiguration configuration;
255255
try {
256-
configuration =
257-
RemoteConfigServiceImpl.UniversalFlagConfigDeserializer.INSTANCE.deserializeApiResponse(
258-
response.body);
256+
configuration = JsonApiUfcResponseParser.INSTANCE.parse(response.body);
259257
} catch (final IOException | RuntimeException e) {
260258
LOGGER.debug("Feature Flagging HTTP configuration source returned malformed UFC payload", e);
261259
return false;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.datadog.featureflag;
2+
3+
import com.squareup.moshi.JsonDataException;
4+
import com.squareup.moshi.JsonReader;
5+
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
6+
import java.io.ByteArrayInputStream;
7+
import java.io.IOException;
8+
import javax.annotation.Nullable;
9+
import okio.BufferedSource;
10+
import okio.Okio;
11+
12+
final class JsonApiUfcResponseParser {
13+
14+
private static final String UNIVERSAL_FLAG_CONFIGURATION_TYPE = "universal-flag-configuration";
15+
private static final JsonReader.Options RESPONSE_FIELDS = JsonReader.Options.of("data");
16+
private static final JsonReader.Options DATA_FIELDS = JsonReader.Options.of("type", "attributes");
17+
18+
static final JsonApiUfcResponseParser INSTANCE =
19+
new JsonApiUfcResponseParser(UniversalFlagConfigParser.INSTANCE);
20+
21+
private final UniversalFlagConfigParser ufcParser;
22+
23+
JsonApiUfcResponseParser(final UniversalFlagConfigParser ufcParser) {
24+
this.ufcParser = ufcParser;
25+
}
26+
27+
@Nullable
28+
ServerConfiguration parse(final byte[] content) throws IOException {
29+
try (BufferedSource source = Okio.buffer(Okio.source(new ByteArrayInputStream(content)))) {
30+
final JsonReader reader = JsonReader.of(source);
31+
if (reader.peek() != JsonReader.Token.BEGIN_OBJECT) {
32+
reader.skipValue();
33+
return null;
34+
}
35+
ServerConfiguration configuration = null;
36+
reader.beginObject();
37+
while (reader.hasNext()) {
38+
if (reader.selectName(RESPONSE_FIELDS) == 0) {
39+
configuration = parseData(reader);
40+
} else {
41+
reader.skipName();
42+
reader.skipValue();
43+
}
44+
}
45+
reader.endObject();
46+
requireEndOfDocument(reader);
47+
return configuration;
48+
}
49+
}
50+
51+
@Nullable
52+
private ServerConfiguration parseData(final JsonReader reader) throws IOException {
53+
if (reader.peek() != JsonReader.Token.BEGIN_OBJECT) {
54+
reader.skipValue();
55+
return null;
56+
}
57+
String type = null;
58+
ServerConfiguration configuration = null;
59+
reader.beginObject();
60+
while (reader.hasNext()) {
61+
switch (reader.selectName(DATA_FIELDS)) {
62+
case 0:
63+
if (reader.peek() == JsonReader.Token.STRING) {
64+
type = reader.nextString();
65+
} else {
66+
reader.skipValue();
67+
}
68+
break;
69+
case 1:
70+
configuration = ufcParser.parse(reader);
71+
break;
72+
default:
73+
reader.skipName();
74+
reader.skipValue();
75+
}
76+
}
77+
reader.endObject();
78+
return UNIVERSAL_FLAG_CONFIGURATION_TYPE.equals(type)
79+
? validConfiguration(configuration)
80+
: null;
81+
}
82+
83+
@Nullable
84+
private static ServerConfiguration validConfiguration(
85+
@Nullable final ServerConfiguration configuration) {
86+
return configuration != null && configuration.flags != null ? configuration : null;
87+
}
88+
89+
private static void requireEndOfDocument(final JsonReader reader) throws IOException {
90+
if (reader.peek() != JsonReader.Token.END_DOCUMENT) {
91+
throw new JsonDataException("JSON document was not fully consumed");
92+
}
93+
}
94+
}
Lines changed: 1 addition & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,15 @@
11
package com.datadog.featureflag;
22

3-
import com.squareup.moshi.JsonAdapter;
4-
import com.squareup.moshi.JsonDataException;
5-
import com.squareup.moshi.JsonReader;
6-
import com.squareup.moshi.JsonWriter;
7-
import com.squareup.moshi.Moshi;
8-
import com.squareup.moshi.Types;
93
import datadog.communication.ddagent.SharedCommunicationObjects;
104
import datadog.remoteconfig.Capabilities;
115
import datadog.remoteconfig.ConfigurationChangesTypedListener;
12-
import datadog.remoteconfig.ConfigurationDeserializer;
136
import datadog.remoteconfig.ConfigurationPoller;
147
import datadog.remoteconfig.PollingRateHinter;
158
import datadog.remoteconfig.Product;
169
import datadog.trace.api.Config;
1710
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
18-
import datadog.trace.api.featureflag.ufc.v1.Flag;
1911
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
20-
import java.io.ByteArrayInputStream;
21-
import java.io.IOException;
22-
import java.lang.annotation.Annotation;
23-
import java.lang.reflect.Type;
24-
import java.time.Instant;
25-
import java.time.format.DateTimeFormatter;
26-
import java.util.Date;
27-
import java.util.HashMap;
28-
import java.util.Map;
29-
import java.util.Set;
30-
import javax.annotation.Nonnull;
3112
import javax.annotation.Nullable;
32-
import okio.Okio;
3313

3414
public class RemoteConfigServiceImpl
3515
implements ConfigurationSourceService, ConfigurationChangesTypedListener<ServerConfiguration> {
@@ -43,8 +23,7 @@ public RemoteConfigServiceImpl(final SharedCommunicationObjects sco, final Confi
4323
@Override
4424
public void init() {
4525
configurationPoller.addCapabilities(Capabilities.CAPABILITY_FFE_FLAG_CONFIGURATION_RULES);
46-
configurationPoller.addListener(
47-
Product.FFE_FLAGS, UniversalFlagConfigDeserializer.INSTANCE, this);
26+
configurationPoller.addListener(Product.FFE_FLAGS, UniversalFlagConfigParser.INSTANCE, this);
4827
configurationPoller.start();
4928
}
5029

@@ -62,131 +41,4 @@ public void accept(
6241
final PollingRateHinter pollingRateHinter) {
6342
FeatureFlaggingGateway.dispatch(configuration);
6443
}
65-
66-
static class UniversalFlagConfigDeserializer
67-
implements ConfigurationDeserializer<ServerConfiguration> {
68-
69-
private static final String UNIVERSAL_FLAG_CONFIGURATION_TYPE = "universal-flag-configuration";
70-
static final UniversalFlagConfigDeserializer INSTANCE = new UniversalFlagConfigDeserializer();
71-
72-
private static final Moshi MOSHI =
73-
new Moshi.Builder().add(Date.class, new DateAdapter()).add(FlagMapAdapter.FACTORY).build();
74-
private static final JsonAdapter<ServerConfiguration> V1_ADAPTER =
75-
MOSHI.adapter(ServerConfiguration.class);
76-
private static final Type API_RESPONSE_TYPE =
77-
Types.newParameterizedType(Map.class, String.class, Object.class);
78-
private static final JsonAdapter<Map<String, Object>> API_RESPONSE_ADAPTER =
79-
MOSHI.adapter(API_RESPONSE_TYPE);
80-
81-
@Override
82-
public ServerConfiguration deserialize(final byte[] content) throws IOException {
83-
return V1_ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
84-
}
85-
86-
@Nullable
87-
ServerConfiguration deserializeApiResponse(final byte[] content) throws IOException {
88-
final Map<String, Object> response =
89-
API_RESPONSE_ADAPTER.fromJson(
90-
Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
91-
if (response != null && response.containsKey("data")) {
92-
final Object data = response.get("data");
93-
if (!(data instanceof Map)) {
94-
return null;
95-
}
96-
final Map<?, ?> dataAttributes = (Map<?, ?>) data;
97-
return UNIVERSAL_FLAG_CONFIGURATION_TYPE.equals(dataAttributes.get("type"))
98-
? validConfiguration(V1_ADAPTER.fromJsonValue(dataAttributes.get("attributes")))
99-
: null;
100-
}
101-
return null;
102-
}
103-
104-
@Nullable
105-
private static ServerConfiguration validConfiguration(
106-
@Nullable final ServerConfiguration configuration) {
107-
return configuration != null && configuration.flags != null ? configuration : null;
108-
}
109-
}
110-
111-
static class FlagMapAdapter extends JsonAdapter<Map<String, Flag>> {
112-
113-
private static final Type FLAGS_TYPE =
114-
Types.newParameterizedType(Map.class, String.class, Flag.class);
115-
116-
static final Factory FACTORY =
117-
new Factory() {
118-
@Nullable
119-
@Override
120-
public JsonAdapter<?> create(
121-
@Nonnull final Type type,
122-
@Nonnull final Set<? extends Annotation> annotations,
123-
@Nonnull final Moshi moshi) {
124-
if (!annotations.isEmpty() || !Types.equals(type, FLAGS_TYPE)) {
125-
return null;
126-
}
127-
return new FlagMapAdapter(moshi.adapter(Flag.class));
128-
}
129-
};
130-
131-
private final JsonAdapter<Flag> flagAdapter;
132-
133-
FlagMapAdapter(final JsonAdapter<Flag> flagAdapter) {
134-
this.flagAdapter = flagAdapter;
135-
}
136-
137-
@Nullable
138-
@Override
139-
public Map<String, Flag> fromJson(@Nonnull final JsonReader reader) throws IOException {
140-
if (reader.peek() == JsonReader.Token.NULL) {
141-
return reader.nextNull();
142-
}
143-
final Map<String, Flag> flags = new HashMap<>();
144-
reader.beginObject();
145-
while (reader.hasNext()) {
146-
final String flagKey = reader.nextName();
147-
final Object rawFlag = reader.readJsonValue();
148-
try {
149-
final Flag flag = flagAdapter.fromJsonValue(rawFlag);
150-
if (flag != null) {
151-
flags.put(flagKey, flag);
152-
}
153-
} catch (JsonDataException | IllegalArgumentException ignored) {
154-
// A malformed flag must not prevent other flags in the same config from evaluating.
155-
}
156-
}
157-
reader.endObject();
158-
return flags;
159-
}
160-
161-
@Override
162-
public void toJson(@Nonnull final JsonWriter writer, @Nullable final Map<String, Flag> value)
163-
throws IOException {
164-
throw new UnsupportedOperationException("Reading only adapter");
165-
}
166-
}
167-
168-
static class DateAdapter extends JsonAdapter<Date> {
169-
170-
@Nullable
171-
@Override
172-
public Date fromJson(@Nonnull final JsonReader reader) throws IOException {
173-
final String date = reader.nextString();
174-
if (date == null) {
175-
return null;
176-
}
177-
try {
178-
final Instant instant = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(date, Instant::from);
179-
return Date.from(instant);
180-
} catch (Exception e) {
181-
// ignore wrongly set dates
182-
return null;
183-
}
184-
}
185-
186-
@Override
187-
public void toJson(@Nonnull final JsonWriter writer, @Nullable final Date value)
188-
throws IOException {
189-
throw new UnsupportedOperationException("Reading only adapter");
190-
}
191-
}
19244
}

0 commit comments

Comments
 (0)