
com.launchdarkly.client.RedisFeatureStore Maven / Gradle / Ivy
Show all versions of launchdarkly-client Show documentation
package com.launchdarkly.client;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.CacheStats;
import com.launchdarkly.client.utils.CachingStoreWrapper;
import com.launchdarkly.client.utils.FeatureStoreCore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.launchdarkly.client.utils.FeatureStoreHelpers.marshalJson;
import static com.launchdarkly.client.utils.FeatureStoreHelpers.unmarshalJson;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Transaction;
/**
* An implementation of {@link FeatureStore} backed by Redis. Also
* supports an optional in-memory cache configuration that can be used to improve performance.
*/
public class RedisFeatureStore implements FeatureStore {
private static final Logger logger = LoggerFactory.getLogger(RedisFeatureStore.class);
// Note that we could avoid the indirection of delegating everything to CachingStoreWrapper if we
// simply returned the wrapper itself as the FeatureStore; however, for historical reasons we can't,
// because we have already exposed the RedisFeatureStore type.
private final CachingStoreWrapper wrapper;
private final Core core;
@Override
public void init(Map, Map> allData) {
wrapper.init(allData);
}
@Override
public T get(VersionedDataKind kind, String key) {
return wrapper.get(kind, key);
}
@Override
public Map all(VersionedDataKind kind) {
return wrapper.all(kind);
}
@Override
public void upsert(VersionedDataKind kind, T item) {
wrapper.upsert(kind, item);
}
@Override
public void delete(VersionedDataKind kind, String key, int version) {
wrapper.delete(kind, key, version);
}
@Override
public boolean initialized() {
return wrapper.initialized();
}
@Override
public void close() throws IOException {
wrapper.close();
}
/**
* Return the underlying Guava cache stats object.
*
* @return the cache statistics object.
*/
public CacheStats getCacheStats() {
return wrapper.getCacheStats();
}
/**
* Creates a new store instance that connects to Redis based on the provided {@link RedisFeatureStoreBuilder}.
*
* See the {@link RedisFeatureStoreBuilder} for information on available configuration options and what they do.
*
* @param builder the configured builder to construct the store with.
*/
protected RedisFeatureStore(RedisFeatureStoreBuilder builder) {
JedisPoolConfig poolConfig = (builder.poolConfig != null) ? builder.poolConfig : new JedisPoolConfig();
JedisPool pool = new JedisPool(poolConfig, builder.uri, builder.connectTimeout, builder.socketTimeout);
String prefix = (builder.prefix == null || builder.prefix.isEmpty()) ?
RedisFeatureStoreBuilder.DEFAULT_PREFIX :
builder.prefix;
this.core = new Core(pool, prefix);
this.wrapper = CachingStoreWrapper.builder(this.core).caching(builder.caching)
.build();
}
/**
* Creates a new store instance that connects to Redis with a default connection (localhost port 6379) and no in-memory cache.
* @deprecated Please use {@link Components#redisFeatureStore()} instead.
*/
public RedisFeatureStore() {
JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");
this.core = new Core(pool, RedisFeatureStoreBuilder.DEFAULT_PREFIX);
this.wrapper = CachingStoreWrapper.builder(this.core).build();
}
static class Core implements FeatureStoreCore {
private final JedisPool pool;
private final String prefix;
private UpdateListener updateListener;
Core(JedisPool pool, String prefix) {
this.pool = pool;
this.prefix = prefix;
}
@Override
public VersionedData getInternal(VersionedDataKind> kind, String key) {
try (Jedis jedis = pool.getResource()) {
VersionedData item = getRedis(kind, key, jedis);
if (item != null) {
logger.debug("[get] Key: {} with version: {} found in \"{}\".", key, item.getVersion(), kind.getNamespace());
}
return item;
}
}
@Override
public Map getAllInternal(VersionedDataKind> kind) {
try (Jedis jedis = pool.getResource()) {
Map allJson = jedis.hgetAll(itemsKey(kind));
Map result = new HashMap<>();
for (Map.Entry entry : allJson.entrySet()) {
VersionedData item = unmarshalJson(kind, entry.getValue());
result.put(entry.getKey(), item);
}
return result;
}
}
@Override
public void initInternal(Map, Map> allData) {
try (Jedis jedis = pool.getResource()) {
Transaction t = jedis.multi();
for (Map.Entry, Map> entry: allData.entrySet()) {
String baseKey = itemsKey(entry.getKey());
t.del(baseKey);
for (VersionedData item: entry.getValue().values()) {
t.hset(baseKey, item.getKey(), marshalJson(item));
}
}
t.set(initedKey(), "");
t.exec();
}
}
@Override
public VersionedData upsertInternal(VersionedDataKind> kind, VersionedData newItem) {
while (true) {
Jedis jedis = null;
try {
jedis = pool.getResource();
String baseKey = itemsKey(kind);
jedis.watch(baseKey);
if (updateListener != null) {
updateListener.aboutToUpdate(baseKey, newItem.getKey());
}
VersionedData oldItem = getRedis(kind, newItem.getKey(), jedis);
if (oldItem != null && oldItem.getVersion() >= newItem.getVersion()) {
logger.debug("Attempted to {} key: {} version: {}" +
" with a version that is the same or older: {} in \"{}\"",
newItem.isDeleted() ? "delete" : "update",
newItem.getKey(), oldItem.getVersion(), newItem.getVersion(), kind.getNamespace());
return oldItem;
}
Transaction tx = jedis.multi();
tx.hset(baseKey, newItem.getKey(), marshalJson(newItem));
List