Skip to content

Commit 98c2cbe

Browse files
authored
discard and log error about data items that are too big to store (#16)
1 parent 3870efd commit 98c2cbe

File tree

3 files changed

+231
-3
lines changed

3 files changed

+231
-3
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ The LaunchDarkly SDK has a standard caching mechanism for any persistent data st
6868
)
6969
.build();
7070

71+
## Data size limitation
72+
73+
DynamoDB has [a 400KB limit](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ServiceQuotas.html#limits-items) on the size of any data item. For the LaunchDarkly SDK, a data item consists of the JSON representation of an individual feature flag or segment configuration, plus a few smaller attributes. You can see the format and size of these representations by querying `https://sdk.launchdarkly.com/flags/latest-all` and setting the `Authorization` header to your SDK key.
74+
75+
Most flags and segments won't be nearly as big as 400KB, but they could be if for instance you have a long list of user keys for individual user targeting. If the flag or segment representation is too large, it cannot be stored in DynamoDB. To avoid disrupting storage and evaluation of other unrelated feature flags, the SDK will simply skip storing that individual flag or segment, and will log a message (at ERROR level) describing the problem. For example:
76+
77+
```
78+
The item "my-flag-key" in "features" was too large to store in DynamoDB and was dropped
79+
```
80+
81+
If caching is enabled in your configuration, the flag or segment may still be available in the SDK from the in-memory cache, but do not rely on this. If you see this message, consider redesigning your flag/segment configurations, or else do not use DynamoDB for the environment that contains this data item.
82+
83+
This limitation does not apply to target lists in [Big Segments](https://docs.launchdarkly.com/home/users/big-segments/).
84+
85+
A future version of the LaunchDarkly DynamoDB integration may use different strategies to work around this limitation, such as compressing the data or dividing it into multiple items. However, this integration is required to be interoperable with the DynamoDB integrations used by all the other LaunchDarkly SDKs and by the Relay Proxy, so any such change will only be made as part of a larger cross-platform release.
86+
7187
## About LaunchDarkly
7288

7389
* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:

src/main/java/com/launchdarkly/sdk/server/integrations/DynamoDbDataStoreImpl.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ final class DynamoDbDataStoreImpl extends DynamoDbStoreImplBase implements Persi
6565
private static final String itemJsonAttribute = "item";
6666
private static final String deletedItemPlaceholder = "null"; // DynamoDB doesn't allow empty strings
6767

68+
// We won't try to store items whose total size exceeds this. The DynamoDB documentation says
69+
// only "400KB", which probably means 400*1024, but to avoid any chance of trying to store a
70+
// too-large item we are rounding it down.
71+
private static final int DYNAMO_DB_MAX_ITEM_SIZE = 400000;
72+
6873
private Runnable updateHook;
6974

7075
DynamoDbDataStoreImpl(DynamoDbClient client, boolean wasExistingClient, String tableName, String prefix) {
@@ -108,6 +113,11 @@ public void init(FullDataSet<SerializedItemDescriptor> allData) {
108113
for (Map.Entry<String, SerializedItemDescriptor> itemEntry: entry.getValue().getItems()) {
109114
String key = itemEntry.getKey();
110115
Map<String, AttributeValue> encodedItem = marshalItem(kind, key, itemEntry.getValue());
116+
117+
if (!checkSizeLimit(encodedItem)) {
118+
continue;
119+
}
120+
111121
requests.add(WriteRequest.builder().putRequest(builder -> builder.item(encodedItem)).build());
112122

113123
Map.Entry<String, String> combinedKey = new AbstractMap.SimpleEntry<>(namespaceForKind(kind), key);
@@ -140,6 +150,9 @@ public void init(FullDataSet<SerializedItemDescriptor> allData) {
140150
@Override
141151
public boolean upsert(DataKind kind, String key, SerializedItemDescriptor newItem) {
142152
Map<String, AttributeValue> encodedItem = marshalItem(kind, key, newItem);
153+
if (!checkSizeLimit(encodedItem)) {
154+
return false;
155+
}
143156

144157
if (updateHook != null) { // instrumentation for tests
145158
updateHook.run();
@@ -271,4 +284,47 @@ static void batchWriteRequests(DynamoDbClient client, String tableName, List<Wri
271284
client.batchWriteItem(builder -> builder.requestItems(batchMap));
272285
}
273286
}
287+
288+
private static boolean checkSizeLimit(Map<String, AttributeValue> item) {
289+
// see: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/CapacityUnitCalculations.html
290+
int size = 100; // fixed overhead for index data
291+
for (Map.Entry<String, AttributeValue> kv: item.entrySet()) {
292+
size += utf8Length(kv.getKey());
293+
if (kv.getValue().s() != null) {
294+
size += utf8Length(kv.getValue().s());
295+
} else if (kv.getValue().n() != null) {
296+
size += utf8Length(kv.getValue().n());
297+
}
298+
}
299+
if (size <= DYNAMO_DB_MAX_ITEM_SIZE) {
300+
return true;
301+
}
302+
logger.error("The item \"{}\" in \"{}\" was too large to store in DynamoDB and was dropped",
303+
item.get(SORT_KEY).s(), item.get(PARTITION_KEY).s());
304+
return false;
305+
}
306+
307+
private static int utf8Length(String s) {
308+
// Unfortunately Java (at least Java 8) doesn't have a built-in way to determine the UTF8 encoding
309+
// length without actually creating a new byte array, which we would rather not do. Guava does
310+
// support this, but we don't want a dependency on Guava (except in test code).
311+
if (s == null) {
312+
return 0;
313+
}
314+
int count = 0;
315+
for (int i = 0, len = s.length(); i < len; i++) {
316+
char ch = s.charAt(i);
317+
if (ch <= 0x7F) {
318+
count++;
319+
} else if (ch <= 0x7FF) {
320+
count += 2;
321+
} else if (Character.isHighSurrogate(ch)) {
322+
count += 4;
323+
++i;
324+
} else {
325+
count += 3;
326+
}
327+
}
328+
return count;
329+
}
274330
}

src/test/java/com/launchdarkly/sdk/server/integrations/DynamoDbDataStoreImplTest.java

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
11
package com.launchdarkly.sdk.server.integrations;
22

3+
import com.google.common.base.Joiner;
4+
import com.google.common.collect.ImmutableList;
5+
import com.google.common.collect.Iterables;
6+
import com.google.common.collect.Lists;
7+
import com.launchdarkly.sdk.server.DataModel;
8+
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind;
9+
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet;
10+
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems;
11+
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.SerializedItemDescriptor;
12+
import com.launchdarkly.sdk.server.interfaces.PersistentDataStore;
13+
14+
import org.junit.BeforeClass;
15+
import org.junit.Test;
16+
17+
import java.util.AbstractMap.SimpleEntry;
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.Map;
21+
322
import static com.launchdarkly.sdk.server.integrations.TestUtils.baseBuilder;
423
import static com.launchdarkly.sdk.server.integrations.TestUtils.clearEverything;
524
import static com.launchdarkly.sdk.server.integrations.TestUtils.createTableIfNecessary;
6-
7-
import org.junit.BeforeClass;
25+
import static org.hamcrest.MatcherAssert.assertThat;
26+
import static org.hamcrest.Matchers.greaterThan;
827

928
/**
1029
* Runs the standard database data store test suite that's defined in the Java SDK.
@@ -17,7 +36,8 @@
1736
*/
1837
@SuppressWarnings("javadoc")
1938
public class DynamoDbDataStoreImplTest extends PersistentDataStoreTestBase<DynamoDbDataStoreImpl> {
20-
39+
private static final String BAD_ITEM_KEY = "baditem";
40+
2141
@BeforeClass
2242
public static void setUpAll() {
2343
createTableIfNecessary();
@@ -43,4 +63,140 @@ protected boolean setUpdateHook(DynamoDbDataStoreImpl storeUnderTest, final Runn
4363
storeUnderTest.setUpdateHook(hook);
4464
return true;
4565
}
66+
67+
@Test
68+
public void dataStoreSkipsAndLogsTooLargeItemOnInitForFlag() throws Exception {
69+
dataStoreSkipsAndLogsTooLargeItemOnInit(DataModel.FEATURES, 0);
70+
}
71+
72+
@Test
73+
public void dataStoreSkipsAndLogsTooLargeItemOnInitForSegment() throws Exception {
74+
dataStoreSkipsAndLogsTooLargeItemOnInit(DataModel.SEGMENTS, 1);
75+
}
76+
77+
@Test
78+
public void dataStoreSkipsAndLogsTooLargeItemOnUpsertForFlag() throws Exception {
79+
dataStoreSkipsAndLogsTooLargeItemOnUpsert(DataModel.FEATURES);
80+
}
81+
82+
@Test
83+
public void dataStoreSkipsAndLogsTooLargeItemOnUpsertForSegment() throws Exception {
84+
dataStoreSkipsAndLogsTooLargeItemOnUpsert(DataModel.SEGMENTS);
85+
}
86+
87+
private void dataStoreSkipsAndLogsTooLargeItemOnInit(
88+
DataKind dataKind,
89+
int collIndex
90+
) throws Exception {
91+
SerializedItemDescriptor badItem = makeTooBigItem(dataKind);
92+
93+
// Construct a data set that is based on the hard-coded data from makeGoodData(), but with one
94+
// oversized item inserted in either the flags collection or the segments collection.
95+
FullDataSet<SerializedItemDescriptor> goodData = makeGoodData();
96+
List<Map.Entry<DataKind, KeyedItems<SerializedItemDescriptor>>> dataPlusBadItem =
97+
Lists.newArrayList(goodData.getData()); // converting the data set to a mutable list
98+
List<Map.Entry<String, SerializedItemDescriptor>> items = Lists.newArrayList(
99+
dataPlusBadItem.get(collIndex).getValue().getItems()); // this is now either list of flags or list of segments
100+
items.add(0, new SimpleEntry<>(BAD_ITEM_KEY, badItem));
101+
// putting the bad item first to prove that items after that one are still stored
102+
dataPlusBadItem.set(collIndex, new SimpleEntry<>(dataKind, new KeyedItems<>(items)));
103+
104+
// Initialize the store with this data set. It should not throw an exception, but instead just
105+
// log an error and store all the *other* items-- so the resulting state should be the same as
106+
// makeGoodData().
107+
try (PersistentDataStore store = makeStore()) {
108+
store.init(new FullDataSet<>(dataPlusBadItem));
109+
110+
assertDataSetsEqual(goodData, getAllData(store));
111+
}
112+
}
113+
114+
private void dataStoreSkipsAndLogsTooLargeItemOnUpsert(
115+
DataKind dataKind
116+
) throws Exception {
117+
FullDataSet<SerializedItemDescriptor> goodData = makeGoodData();
118+
119+
// Initialize the store with valid data.
120+
try (PersistentDataStore store = makeStore()) {
121+
store.init(goodData);
122+
123+
assertDataSetsEqual(goodData, getAllData(store));
124+
125+
// Now try to store an oversized item. It should not throw an exception, but should not do
126+
// the update-- so the resulting state should be the same valid data as before.
127+
store.upsert(dataKind, BAD_ITEM_KEY, makeTooBigItem(dataKind));
128+
129+
assertDataSetsEqual(goodData, getAllData(store));
130+
}
131+
}
132+
133+
private static FullDataSet<SerializedItemDescriptor> makeGoodData() {
134+
return new FullDataSet<SerializedItemDescriptor>(ImmutableList.of(
135+
new SimpleEntry<>(DataModel.FEATURES,
136+
new KeyedItems<SerializedItemDescriptor>(ImmutableList.of(
137+
new SimpleEntry<>("flag1",
138+
new SerializedItemDescriptor(1, false, "{\"key\": \"flag1\", \"version\": 1}")),
139+
new SimpleEntry<>("flag2",
140+
new SerializedItemDescriptor(1, false, "{\"key\": \"flag2\", \"version\": 1}"))
141+
))),
142+
new SimpleEntry<>(DataModel.SEGMENTS,
143+
new KeyedItems<SerializedItemDescriptor>(ImmutableList.of(
144+
new SimpleEntry<>("segment1",
145+
new SerializedItemDescriptor(1, false, "{\"key\": \"segment1\", \"version\": 1}")),
146+
new SimpleEntry<>("segment2",
147+
new SerializedItemDescriptor(1, false, "{\"key\": \"segment2\", \"version\": 1}"))
148+
)))
149+
));
150+
}
151+
152+
private static SerializedItemDescriptor makeTooBigItem(DataKind dataKind) {
153+
StringBuilder tooBigKeysListJson = new StringBuilder().append('[');
154+
for (int i = 0; i < 40000; i++) {
155+
if (i != 0 ) {
156+
tooBigKeysListJson.append(',');
157+
}
158+
tooBigKeysListJson.append("\"key").append(i).append('"');
159+
}
160+
tooBigKeysListJson.append(']');
161+
assertThat(tooBigKeysListJson.length(), greaterThan(400 * 1024));
162+
163+
String serializedItem;
164+
if (dataKind == DataModel.SEGMENTS) {
165+
serializedItem = "{\":key\":\"" + BAD_ITEM_KEY + "\", \"version\": 1, \"included\": " +
166+
tooBigKeysListJson.toString() + "}";
167+
} else {
168+
serializedItem = "{\":key\":\"" + BAD_ITEM_KEY + "\", \"version\": 1, \"targets\": [{\"variation\": 0, \"values\":" +
169+
tooBigKeysListJson.toString() + "}]}";
170+
}
171+
return new SerializedItemDescriptor(1, false, serializedItem);
172+
}
173+
174+
private static FullDataSet<SerializedItemDescriptor> getAllData(PersistentDataStore store) {
175+
List<Map.Entry<DataKind, KeyedItems<SerializedItemDescriptor>>> colls = new ArrayList<>();
176+
for (DataKind kind: new DataKind[] { DataModel.FEATURES, DataModel.SEGMENTS }) {
177+
KeyedItems<SerializedItemDescriptor> items = store.getAll(kind);
178+
colls.add(new SimpleEntry<>(kind, items));
179+
}
180+
return new FullDataSet<>(colls);
181+
}
182+
183+
private static void assertDataSetsEqual(FullDataSet<SerializedItemDescriptor> expected,
184+
FullDataSet<SerializedItemDescriptor> actual) {
185+
if (!actual.equals(expected)) {
186+
throw new AssertionError("expected " + describeDataSet(expected) + " but got " +
187+
describeDataSet(actual));
188+
}
189+
}
190+
191+
private static String describeDataSet(FullDataSet<SerializedItemDescriptor> data) {
192+
return Joiner.on(", ").join(
193+
Iterables.transform(data.getData(), entry -> {
194+
DataKind kind = entry.getKey();
195+
return "{" + kind + ": [" +
196+
Joiner.on(", ").join(
197+
Iterables.transform(entry.getValue().getItems(), item -> item.getValue().getSerializedItem())
198+
) +
199+
"]}";
200+
}));
201+
}
46202
}

0 commit comments

Comments
 (0)