
com.launchdarkly.client.integrations.ConsulDataStoreImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of launchdarkly-java-server-sdk-consul-store Show documentation
Show all versions of launchdarkly-java-server-sdk-consul-store Show documentation
LaunchDarkly Java SDK Consul integration
package com.launchdarkly.client.integrations;
import com.launchdarkly.client.VersionedData;
import com.launchdarkly.client.VersionedDataKind;
import com.launchdarkly.client.utils.FeatureStoreCore;
import com.launchdarkly.client.utils.FeatureStoreHelpers;
import com.orbitz.consul.Consul;
import com.orbitz.consul.ConsulException;
import com.orbitz.consul.model.kv.Operation;
import com.orbitz.consul.model.kv.Value;
import com.orbitz.consul.model.kv.Verb;
import com.orbitz.consul.option.ImmutablePutOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* Internal implementation of the Consul feature store.
*
* Implementation notes:
*
* - Feature flags, segments, and any other kind of entity the LaunchDarkly client may wish
* to store, are stored as individual items with the key "{prefix}/features/{flag-key}",
* "{prefix}/segments/{segment-key}", etc.
*
- The special key "{prefix}/$inited" indicates that the store contains a complete data set.
*
- Since Consul has limited support for transactions (they can't contain more than 64
* operations), the Init method-- which replaces the entire data store-- is not guaranteed to
* be atomic, so there can be a race condition if another process is adding new data via
* Upsert. To minimize this, we don't delete all the data at the start; instead, we update
* the items we've received, and then delete all other items. That could potentially result in
* deleting new data from another process, but that would be the case anyway if the Init
* happened to execute later than the Upsert; we are relying on the fact that normally the
* process that did the Init will also receive the new data shortly and do its own Upsert.
*
*/
class ConsulDataStoreImpl implements FeatureStoreCore {
private static final Logger logger = LoggerFactory.getLogger(ConsulDataStoreImpl.class);
private final Consul client;
private final String prefix;
ConsulDataStoreImpl(Consul client, String prefix) {
this.client = client;
this.prefix = prefix + "/";
}
@Override
public void close() throws IOException {
client.destroy();
}
@Override
public VersionedData getInternal(VersionedDataKind> kind, String key) {
Optional value = client.keyValueClient().getValueAsString(itemKey(kind, key));
return value.map(s -> FeatureStoreHelpers.unmarshalJson(kind, s)).orElse(null);
}
@Override
public Map getAllInternal(VersionedDataKind> kind) {
Map itemsOut = new HashMap<>();
for (String value: client.keyValueClient().getValuesAsString(kindKey(kind))) {
VersionedData item = FeatureStoreHelpers.unmarshalJson(kind, value);
itemsOut.put(item.getKey(), item);
}
return itemsOut;
}
@Override
public void initInternal(Map, Map> allData) {
// Start by reading the existing keys; we will later delete any of these that weren't in allData.
Set unusedOldKeys = new HashSet<>();
try {
unusedOldKeys.addAll(client.keyValueClient().getKeys(prefix));
} catch (ConsulException e) {
// Annoyingly, if no keys currently exist, the client throws an exception instead of just returning an empty list
if (e.getCode() != 404) {
throw e;
}
}
List ops = new ArrayList<>();
int numItems = 0;
// Insert or update every provided item
for (Map.Entry, Map> entry: allData.entrySet()) {
VersionedDataKind> kind = entry.getKey();
for (VersionedData item: entry.getValue().values()) {
String json = FeatureStoreHelpers.marshalJson(item);
String key = itemKey(kind, item.getKey());
Operation op = Operation.builder(Verb.SET).key(key).value(json).build();
ops.add(op);
unusedOldKeys.remove(key);
numItems++;
}
}
// Now delete any previously existing items whose keys were not in the current data
for (String key: unusedOldKeys) {
if (!key.equals(initedKey())) {
Operation op = Operation.builder(Verb.DELETE).key(key).build();
ops.add(op);
}
}
// Now set the special key that we check in initializedInternal()
Operation op = Operation.builder(Verb.SET).key(initedKey()).value("").build();
ops.add(op);
batchOperations(ops);
logger.info("Initialized database with {} items", numItems);
}
@Override
public VersionedData upsertInternal(VersionedDataKind> kind, VersionedData newItem) {
String key = itemKey(kind, newItem.getKey());
String json = FeatureStoreHelpers.marshalJson(newItem);
// We will potentially keep retrying indefinitely until someone's write succeeds
while (true) {
Optional oldValue = client.keyValueClient().getValue(key);
VersionedData oldItem = oldValue.flatMap(v -> v.getValueAsString()).map(
s -> FeatureStoreHelpers.unmarshalJson(kind, s)).orElse(null);
// Check whether the item is stale. If so, don't do the update (and return the existing item to
// FeatureStoreWrapper so it can be cached)
if (oldItem != null && oldItem.getVersion() >= newItem.getVersion()) {
return oldItem;
}
// Otherwise, try to write. We will do a compare-and-set operation, so the write will only succeed if
// the key's ModifyIndex is still equal to the previous value returned by getEvenIfDeleted. If the
// previous ModifyIndex was zero, it means the key did not previously exist and the write will only
// succeed if it still doesn't exist.
long modifyIndex = oldValue.map(v -> v.getModifyIndex()).orElse(0L);
boolean success = client.keyValueClient().putValue(key, json, 0,
ImmutablePutOptions.builder().cas(modifyIndex).build());
if (success) {
return newItem;
}
// If we failed, retry the whole shebang
logger.debug("Concurrent modification detected, retrying");
}
}
@Override
public boolean initializedInternal() {
return client.keyValueClient().getValue(initedKey()).isPresent();
}
private String kindKey(VersionedDataKind> kind) {
return prefix + kind.getNamespace();
}
private String itemKey(VersionedDataKind> kind, String key) {
return kindKey(kind) + "/" + key;
}
private String initedKey() {
return prefix + "$inited";
}
private void batchOperations(List ops) {
int batchSize = 64; // Consul can only do this many at a time
for (int i = 0; i < ops.size(); i += batchSize) {
int limit = (i + batchSize < ops.size()) ? (i + batchSize) : ops.size();
List batch = ops.subList(i, limit);
client.keyValueClient().performTransaction(batch.toArray(new Operation[batch.size()]));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy