Skip to content
This repository has been archived by the owner on May 30, 2024. It is now read-only.

Commit

Permalink
Merge pull request #165 from launchdarkly/eb/ch59586/integrations-pac…
Browse files Browse the repository at this point in the history
…kage

(4.x - #2) move Redis and FileData stuff into new integrations subpackage
  • Loading branch information
eli-darkly authored Jan 7, 2020
2 parents c273a05 + c754fdf commit 9cd5228
Show file tree
Hide file tree
Showing 35 changed files with 1,200 additions and 886 deletions.
13 changes: 7 additions & 6 deletions src/main/java/com/launchdarkly/client/Components.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,23 @@ public static FeatureStoreFactory inMemoryFeatureStore() {
}

/**
* Returns a factory with builder methods for creating a Redis-backed implementation of {@link FeatureStore},
* using {@link RedisFeatureStoreBuilder#DEFAULT_URI}.
* Deprecated name for {@link com.launchdarkly.client.integrations.Redis#dataStore()}.
* @return a factory/builder object
* @see LDConfig.Builder#dataStore(FeatureStoreFactory)
* @deprecated Use {@link com.launchdarkly.client.integrations.Redis#dataStore()}.
*/
@Deprecated
public static RedisFeatureStoreBuilder redisFeatureStore() {
return new RedisFeatureStoreBuilder();
}

/**
* Returns a factory with builder methods for creating a Redis-backed implementation of {@link FeatureStore},
* specifying the Redis URI.
* Deprecated name for {@link com.launchdarkly.client.integrations.Redis#dataStore()}.
* @param redisUri the URI of the Redis host
* @return a factory/builder object
* @see LDConfig.Builder#dataStore(FeatureStoreFactory)
* @deprecated Use {@link com.launchdarkly.client.integrations.Redis#dataStore()} and
* {@link com.launchdarkly.client.integrations.RedisDataStoreBuilder#uri(URI)}.
*/
@Deprecated
public static RedisFeatureStoreBuilder redisFeatureStore(URI redisUri) {
return new RedisFeatureStoreBuilder(redisUri);
}
Expand Down
232 changes: 20 additions & 212 deletions src/main/java/com/launchdarkly/client/RedisFeatureStore.java
Original file line number Diff line number Diff line change
@@ -1,73 +1,58 @@
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;
import redis.clients.util.JedisURIHelper;

/**
* An implementation of {@link FeatureStore} backed by Redis. Also
* supports an optional in-memory cache configuration that can be used to improve performance.
* Deprecated implementation class for the Redis-based persistent data store.
* <p>
* Instead of referencing this class directly, use {@link com.launchdarkly.client.integrations.Redis#dataStore()} to obtain a builder object.
*
* @deprecated Use {@link com.launchdarkly.client.integrations.Redis#dataStore()}
*/
@Deprecated
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;
// The actual implementation is now in the com.launchdarkly.integrations package. This class remains
// visible for backward compatibility, but simply delegates to an instance of the underlying store.

private final FeatureStore wrappedStore;

@Override
public void init(Map<VersionedDataKind<?>, Map<String, ? extends VersionedData>> allData) {
wrapper.init(allData);
wrappedStore.init(allData);
}

@Override
public <T extends VersionedData> T get(VersionedDataKind<T> kind, String key) {
return wrapper.get(kind, key);
return wrappedStore.get(kind, key);
}

@Override
public <T extends VersionedData> Map<String, T> all(VersionedDataKind<T> kind) {
return wrapper.all(kind);
return wrappedStore.all(kind);
}

@Override
public <T extends VersionedData> void upsert(VersionedDataKind<T> kind, T item) {
wrapper.upsert(kind, item);
wrappedStore.upsert(kind, item);
}

@Override
public <T extends VersionedData> void delete(VersionedDataKind<T> kind, String key, int version) {
wrapper.delete(kind, key, version);
wrappedStore.delete(kind, key, version);
}

@Override
public boolean initialized() {
return wrapper.initialized();
return wrappedStore.initialized();
}

@Override
public void close() throws IOException {
wrapper.close();
wrappedStore.close();
}

/**
Expand All @@ -76,7 +61,7 @@ public void close() throws IOException {
* @return the cache statistics object.
*/
public CacheStats getCacheStats() {
return wrapper.getCacheStats();
return ((CachingStoreWrapper)wrappedStore).getCacheStats();
}

/**
Expand All @@ -87,192 +72,15 @@ public CacheStats getCacheStats() {
* @param builder the configured builder to construct the store with.
*/
protected RedisFeatureStore(RedisFeatureStoreBuilder builder) {
// There is no builder for JedisPool, just a large number of constructor overloads. Unfortunately,
// the overloads that accept a URI do not accept the other parameters we need to set, so we need
// to decompose the URI.
String host = builder.uri.getHost();
int port = builder.uri.getPort();
String password = builder.password == null ? JedisURIHelper.getPassword(builder.uri) : builder.password;
int database = builder.database == null ? JedisURIHelper.getDBIndex(builder.uri): builder.database.intValue();
boolean tls = builder.tls || builder.uri.getScheme().equals("rediss");

String extra = tls ? " with TLS" : "";
if (password != null) {
extra = extra + (extra.isEmpty() ? " with" : " and") + " password";
}
logger.info(String.format("Connecting to Redis feature store at %s:%d/%d%s", host, port, database, extra));

JedisPoolConfig poolConfig = (builder.poolConfig != null) ? builder.poolConfig : new JedisPoolConfig();
JedisPool pool = new JedisPool(poolConfig,
host,
port,
builder.connectTimeout,
builder.socketTimeout,
password,
database,
null, // clientName
tls,
null, // sslSocketFactory
null, // sslParameters
null // hostnameVerifier
);

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();
wrappedStore = builder.wrappedBuilder.createFeatureStore();
}

/**
* 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.
*/
@Deprecated
public RedisFeatureStore() {
this(new RedisFeatureStoreBuilder().caching(FeatureStoreCacheConfig.disabled()));
}

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<String, VersionedData> getAllInternal(VersionedDataKind<?> kind) {
try (Jedis jedis = pool.getResource()) {
Map<String, String> allJson = jedis.hgetAll(itemsKey(kind));
Map<String, VersionedData> result = new HashMap<>();

for (Map.Entry<String, String> entry : allJson.entrySet()) {
VersionedData item = unmarshalJson(kind, entry.getValue());
result.put(entry.getKey(), item);
}
return result;
}
}

@Override
public void initInternal(Map<VersionedDataKind<?>, Map<String, VersionedData>> allData) {
try (Jedis jedis = pool.getResource()) {
Transaction t = jedis.multi();

for (Map.Entry<VersionedDataKind<?>, Map<String, VersionedData>> 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<Object> result = tx.exec();
if (result.isEmpty()) {
// if exec failed, it means the watch was triggered and we should retry
logger.debug("Concurrent modification detected, retrying");
continue;
}

return newItem;
} finally {
if (jedis != null) {
jedis.unwatch();
jedis.close();
}
}
}
}

@Override
public boolean initializedInternal() {
try (Jedis jedis = pool.getResource()) {
return jedis.exists(initedKey());
}
}

@Override
public void close() throws IOException {
logger.info("Closing LaunchDarkly RedisFeatureStore");
pool.destroy();
}

@VisibleForTesting
void setUpdateListener(UpdateListener updateListener) {
this.updateListener = updateListener;
}

private String itemsKey(VersionedDataKind<?> kind) {
return prefix + ":" + kind.getNamespace();
}

private String initedKey() {
return prefix + ":$inited";
}

private <T extends VersionedData> T getRedis(VersionedDataKind<T> kind, String key, Jedis jedis) {
String json = jedis.hget(itemsKey(kind), key);

if (json == null) {
logger.debug("[get] Key: {} not found in \"{}\". Returning null", key, kind.getNamespace());
return null;
}

return unmarshalJson(kind, json);
}
}

static interface UpdateListener {
void aboutToUpdate(String baseKey, String itemKey);
}

@VisibleForTesting
void setUpdateListener(UpdateListener updateListener) {
core.setUpdateListener(updateListener);
}
}
Loading

0 comments on commit 9cd5228

Please sign in to comment.