Skip to content

Commit 50d4959

Browse files
authored
chore: adds transactional memory store (#103)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions Not possible yet **Related issues** SDK-1619 **Describe the solution you've provided** Porting existing Dotnet SDK Impl
1 parent 5be699e commit 50d4959

6 files changed

Lines changed: 1002 additions & 11 deletions

File tree

lib/sdk/server/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ ext.versions = [
7171
"guava": "32.0.1-jre",
7272
"jackson": "2.11.2",
7373
"launchdarklyJavaSdkCommon": "2.1.2",
74-
"launchdarklyJavaSdkInternal": "1.5.1",
74+
"launchdarklyJavaSdkInternal": "1.6.1",
7575
"launchdarklyLogging": "1.1.0",
7676
"okhttp": "4.12.0", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource
7777
"okhttpEventsource": "4.1.0",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.launchdarkly.sdk.server;
2+
3+
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet;
4+
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor;
5+
6+
/**
7+
* Optional interface for data stores that can export their entire contents.
8+
* <p>
9+
* This interface is used to enable recovery scenarios where a persistent store
10+
* needs to be re-synchronized from an in-memory cache. Not all data stores need
11+
* to implement this interface.
12+
* <p>
13+
* This is currently only for internal implementations.
14+
*/
15+
interface CacheExporter {
16+
/**
17+
* Exports all data from the cache across all known DataKinds.
18+
*
19+
* @return A FullDataSet containing all items in the cache. The data is a snapshot
20+
* taken at the time of the call and may be stale immediately after return.
21+
*/
22+
FullDataSet<ItemDescriptor> exportAll();
23+
24+
/**
25+
* Indicates if the cache has been populated with a full data set.
26+
*
27+
* @return true when the cache has been populated
28+
*/
29+
boolean isInitialized();
30+
}
31+

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/InMemoryDataStore.java

Lines changed: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22

33
import com.google.common.collect.ImmutableList;
44
import com.google.common.collect.ImmutableMap;
5+
import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
56
import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider.CacheStats;
67
import com.launchdarkly.sdk.server.subsystems.DataStore;
8+
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ChangeSet;
9+
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ChangeSetType;
710
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind;
811
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet;
912
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor;
1013
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems;
14+
import com.launchdarkly.sdk.server.subsystems.TransactionalDataStore;
1115

1216
import java.io.IOException;
17+
import java.util.AbstractMap;
1318
import java.util.HashMap;
1419
import java.util.Map;
20+
import java.util.Set;
1521

1622
/**
1723
* A thread-safe, versioned store for feature flags and related data based on a
@@ -20,21 +26,16 @@
2026
* As of version 5.0.0, this is package-private; applications must use the factory method
2127
* {@link Components#inMemoryDataStore()}.
2228
*/
23-
class InMemoryDataStore implements DataStore {
29+
class InMemoryDataStore implements DataStore, TransactionalDataStore, CacheExporter {
2430
private volatile ImmutableMap<DataKind, Map<String, ItemDescriptor>> allData = ImmutableMap.of();
2531
private volatile boolean initialized = false;
2632
private Object writeLock = new Object();
33+
private final Object selectorLock = new Object();
34+
private volatile Selector selector = Selector.EMPTY;
2735

2836
@Override
2937
public void init(FullDataSet<ItemDescriptor> allData) {
30-
synchronized (writeLock) {
31-
ImmutableMap.Builder<DataKind, Map<String, ItemDescriptor>> newData = ImmutableMap.builder();
32-
for (Map.Entry<DataKind, KeyedItems<ItemDescriptor>> entry: allData.getData()) {
33-
newData.put(entry.getKey(), ImmutableMap.copyOf(entry.getValue().getItems()));
34-
}
35-
this.allData = newData.build(); // replaces the entire map atomically
36-
this.initialized = true;
37-
}
38+
applyFullPayload(allData.getData(), null, Selector.EMPTY);
3839
}
3940

4041
@Override
@@ -118,4 +119,115 @@ public CacheStats getCacheStats() {
118119
public void close() throws IOException {
119120
return;
120121
}
122+
123+
@Override
124+
public void apply(ChangeSet<ItemDescriptor> changeSet) {
125+
switch (changeSet.getType()) {
126+
case Full:
127+
applyFullPayload(changeSet.getData(), changeSet.getEnvironmentId(), changeSet.getSelector());
128+
break;
129+
case Partial:
130+
applyPartialData(changeSet.getData(), changeSet.getSelector());
131+
break;
132+
case None:
133+
break;
134+
default:
135+
// This represents an implementation error. The ChangeSetType was extended, but handling was not
136+
// added.
137+
throw new IllegalArgumentException("Unknown ChangeSetType: " + changeSet.getType());
138+
}
139+
}
140+
141+
@Override
142+
public Selector getSelector() {
143+
synchronized (selectorLock) {
144+
return selector;
145+
}
146+
}
147+
148+
private void setSelector(Selector newSelector) {
149+
synchronized (selectorLock) {
150+
selector = newSelector;
151+
}
152+
}
153+
154+
private void applyPartialData(Iterable<Map.Entry<DataKind, KeyedItems<ItemDescriptor>>> data,
155+
Selector selector) {
156+
synchronized (writeLock) {
157+
// Build the complete updated dictionary before assigning to Items for transactional update
158+
ImmutableMap.Builder<DataKind, Map<String, ItemDescriptor>> itemsBuilder = ImmutableMap.builder();
159+
160+
// First, collect all kinds that will be updated
161+
java.util.Set<DataKind> updatedKinds = new java.util.HashSet<>();
162+
for (Map.Entry<DataKind, KeyedItems<ItemDescriptor>> kindItemsPair : data) {
163+
updatedKinds.add(kindItemsPair.getKey());
164+
}
165+
166+
// Add all existing kinds that are NOT being updated
167+
for (Map.Entry<DataKind, Map<String, ItemDescriptor>> existingEntry : allData.entrySet()) {
168+
if (!updatedKinds.contains(existingEntry.getKey())) {
169+
itemsBuilder.put(existingEntry.getKey(), existingEntry.getValue());
170+
}
171+
}
172+
173+
// Now process the updated kinds
174+
for (Map.Entry<DataKind, KeyedItems<ItemDescriptor>> kindItemsPair : data) {
175+
DataKind kind = kindItemsPair.getKey();
176+
// Use HashMap to allow overwriting, then convert to ImmutableMap
177+
Map<String, ItemDescriptor> kindMap = new HashMap<>();
178+
179+
Map<String, ItemDescriptor> itemsOfKind = allData.get(kind);
180+
if (itemsOfKind != null) {
181+
kindMap.putAll(itemsOfKind);
182+
}
183+
184+
// Overwrite/add items from the change set (HashMap.put overwrites existing keys)
185+
for (Map.Entry<String, ItemDescriptor> keyValuePair : kindItemsPair.getValue().getItems()) {
186+
kindMap.put(keyValuePair.getKey(), keyValuePair.getValue());
187+
}
188+
189+
itemsBuilder.put(kind, ImmutableMap.copyOf(kindMap));
190+
}
191+
192+
allData = itemsBuilder.build();
193+
setSelector(selector);
194+
}
195+
}
196+
197+
private void applyFullPayload(Iterable<Map.Entry<DataKind, KeyedItems<ItemDescriptor>>> data,
198+
String environmentId, Selector selector) {
199+
ImmutableMap.Builder<DataKind, Map<String, ItemDescriptor>> itemsBuilder = ImmutableMap.builder();
200+
201+
for (Map.Entry<DataKind, KeyedItems<ItemDescriptor>> kindEntry : data) {
202+
ImmutableMap.Builder<String, ItemDescriptor> kindItemsBuilder = ImmutableMap.builder();
203+
for (Map.Entry<String, ItemDescriptor> e1 : kindEntry.getValue().getItems()) {
204+
kindItemsBuilder.put(e1.getKey(), e1.getValue());
205+
}
206+
itemsBuilder.put(kindEntry.getKey(), kindItemsBuilder.build());
207+
}
208+
209+
ImmutableMap<DataKind, Map<String, ItemDescriptor>> newItems = itemsBuilder.build();
210+
211+
synchronized (writeLock) {
212+
allData = newItems;
213+
initialized = true;
214+
setSelector(selector);
215+
}
216+
}
217+
218+
@Override
219+
public FullDataSet<ItemDescriptor> exportAll() {
220+
synchronized (writeLock) {
221+
ImmutableList.Builder<Map.Entry<DataKind, KeyedItems<ItemDescriptor>>> builder = ImmutableList.builder();
222+
223+
for (Map.Entry<DataKind, Map<String, ItemDescriptor>> kindEntry : allData.entrySet()) {
224+
builder.add(new AbstractMap.SimpleEntry<>(
225+
kindEntry.getKey(),
226+
new KeyedItems<>(ImmutableList.copyOf(kindEntry.getValue().entrySet()))
227+
));
228+
}
229+
230+
return new FullDataSet<>(builder.build());
231+
}
232+
}
121233
}

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreTypes.java

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.launchdarkly.sdk.server.subsystems;
22

33
import com.google.common.collect.ImmutableList;
4+
import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
45

56
import java.util.Map;
67
import java.util.Objects;
@@ -328,4 +329,117 @@ public int hashCode() {
328329
return items.hashCode();
329330
}
330331
}
332+
333+
/**
334+
* Enumeration that indicates if this change is a full or partial change.
335+
* <p>
336+
* This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning.
337+
* It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode
338+
* </p>
339+
*/
340+
public enum ChangeSetType {
341+
/**
342+
* Represents a full store configuration which replaces all data currently in the store.
343+
*/
344+
Full,
345+
346+
/**
347+
* Represents an incremental set of changes to be applied to the existing data in the store.
348+
*/
349+
Partial,
350+
351+
/**
352+
* Indicates that there are no store changes.
353+
*/
354+
None
355+
}
356+
357+
/**
358+
* Represents a set of changes to apply to a store.
359+
* <p>
360+
* This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning.
361+
* It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode
362+
* </p>
363+
*
364+
* @param <TItemDescriptor> will be {@link ItemDescriptor} or {@link SerializedItemDescriptor}
365+
*/
366+
public static final class ChangeSet<TItemDescriptor> {
367+
private final ChangeSetType type;
368+
private final Selector selector;
369+
private final String environmentId;
370+
private final Iterable<Map.Entry<DataKind, KeyedItems<TItemDescriptor>>> data;
371+
372+
/**
373+
* Returns the type of the changeset.
374+
*
375+
* @return the changeset type
376+
*/
377+
public ChangeSetType getType() {
378+
return type;
379+
}
380+
381+
/**
382+
* Returns the selector for this change. This selector will not be null, but it can be an empty selector.
383+
*
384+
* @return the selector
385+
*/
386+
public Selector getSelector() {
387+
return selector;
388+
}
389+
390+
/**
391+
* Returns the environment ID associated with the change. This may not always be available, and when it is not,
392+
* the value will be null.
393+
*
394+
* @return the environment ID, or null if not available
395+
*/
396+
public String getEnvironmentId() {
397+
return environmentId;
398+
}
399+
400+
/**
401+
* Returns a list of changes.
402+
*
403+
* @return an enumeration of key-value pairs; may be empty, but will not be null
404+
*/
405+
public Iterable<Map.Entry<DataKind, KeyedItems<TItemDescriptor>>> getData() {
406+
return data;
407+
}
408+
409+
/**
410+
* Constructs a new ChangeSet instance.
411+
*
412+
* @param type the type of the changeset
413+
* @param selector the selector for this change
414+
* @param data the list of changes
415+
* @param environmentId the environment ID, or null if not available
416+
*/
417+
public ChangeSet(ChangeSetType type, Selector selector,
418+
Iterable<Map.Entry<DataKind, KeyedItems<TItemDescriptor>>> data, String environmentId) {
419+
this.type = type;
420+
this.selector = selector;
421+
this.data = data == null ? ImmutableList.of() : data;
422+
this.environmentId = environmentId;
423+
}
424+
425+
@Override
426+
public boolean equals(Object o) {
427+
if (o instanceof ChangeSet<?>) {
428+
ChangeSet<?> other = (ChangeSet<?>)o;
429+
return type == other.type && Objects.equals(selector, other.selector) &&
430+
Objects.equals(environmentId, other.environmentId) && Objects.equals(data, other.data);
431+
}
432+
return false;
433+
}
434+
435+
@Override
436+
public int hashCode() {
437+
return Objects.hash(type, selector, environmentId, data);
438+
}
439+
440+
@Override
441+
public String toString() {
442+
return "ChangeSet(" + type + "," + selector + "," + environmentId + "," + data + ")";
443+
}
444+
}
331445
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.launchdarkly.sdk.server.subsystems;
2+
3+
import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
4+
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ChangeSet;
5+
import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor;
6+
7+
/**
8+
* Interface for a data store that holds feature flags and related data received by the SDK.
9+
* This interface supports updating the store transactionally using ChangeSets.
10+
* <p>
11+
* Ordinarily, the only implementation of this interface is the default in-memory
12+
* implementation, which holds references to actual SDK data model objects. Any data store
13+
* implementation that uses an external store, such as a database, should instead use
14+
* {@link PersistentDataStore}.
15+
* <p>
16+
* Implementations must be thread-safe.
17+
* <p>
18+
* This interface is not stable, and not subject to any backwards compatibility guarantees or semantic versioning.
19+
* It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode
20+
*
21+
* @see PersistentDataStore
22+
*/
23+
public interface TransactionalDataStore {
24+
/**
25+
* Apply the given change set to the store. This should be done atomically if possible.
26+
*
27+
* @param changeSet the changeset to apply
28+
*/
29+
void apply(ChangeSet<ItemDescriptor> changeSet);
30+
31+
/**
32+
* Returns the selector for the currently stored data. The selector will be non-null but may be empty.
33+
*
34+
* @return the selector for the currently stored data
35+
*/
36+
Selector getSelector();
37+
}

0 commit comments

Comments
 (0)