initCache = createInitCache();
private String prefix;
private ListeningExecutorService executorService;
- /**
- * Creates a new store instance that connects to Redis with the provided host, port, prefix, and cache timeout. Uses a default
- * connection pool configuration.
- *
- * @param host the host for the Redis connection
- * @param port the port for the Redis connection
- * @param prefix a namespace prefix for all keys stored in Redis
- * @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
- * @deprecated as of 1.1. Please use the {@link RedisFeatureStoreBuilder#build()} for a more flexible way of constructing a {@link RedisFeatureStore}.
- */
- @Deprecated
- public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSecs) {
- this(host, port, prefix, cacheTimeSecs, getPoolConfig());
- }
-
- /**
- * Creates a new store instance that connects to Redis with the provided URI, prefix, and cache timeout. Uses a default
- * connection pool configuration.
- *
- * @param uri the URI for the Redis connection
- * @param prefix a namespace prefix for all keys stored in Redis
- * @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
- * @deprecated as of 1.1. Please use the {@link RedisFeatureStoreBuilder#build()} for a more flexible way of constructing a {@link RedisFeatureStore}.
- */
- @Deprecated
- public RedisFeatureStore(URI uri, String prefix, long cacheTimeSecs) {
- this(uri, prefix, cacheTimeSecs, getPoolConfig());
- }
-
- /**
- * Creates a new store instance that connects to Redis with the provided host, port, prefix, cache timeout, and connection pool settings.
- *
- * @param host the host for the Redis connection
- * @param port the port for the Redis connection
- * @param prefix a namespace prefix for all keys stored in Redis
- * @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
- * @param poolConfig an optional pool config for the Jedis connection pool
- * @deprecated as of 1.1. Please use the {@link RedisFeatureStoreBuilder#build()} for a more flexible way of constructing a {@link RedisFeatureStore}.
- */
- @Deprecated
- public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSecs, JedisPoolConfig poolConfig) {
- pool = new JedisPool(poolConfig, host, port);
- setPrefix(prefix);
- createCache(cacheTimeSecs);
- }
-
- /**
- * Creates a new store instance that connects to Redis with the provided URI, prefix, cache timeout, and connection pool settings.
- *
- * @param uri the URI for the Redis connection
- * @param prefix a namespace prefix for all keys stored in Redis
- * @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
- * @param poolConfig an optional pool config for the Jedis connection pool
- * @deprecated as of 1.1. Please use the {@link RedisFeatureStoreBuilder#build()} for a more flexible way of constructing a {@link RedisFeatureStore}.
- */
- @Deprecated
- public RedisFeatureStore(URI uri, String prefix, long cacheTimeSecs, JedisPoolConfig poolConfig) {
- pool = new JedisPool(poolConfig, uri);
- setPrefix(prefix);
- createCache(cacheTimeSecs);
+ private static class CacheKey {
+ final VersionedDataKind> kind;
+ final String key;
+
+ public CacheKey(VersionedDataKind> kind, String key) {
+ this.kind = kind;
+ this.key = key;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof CacheKey) {
+ CacheKey o = (CacheKey) other;
+ return o.kind.getNamespace().equals(this.kind.getNamespace()) &&
+ o.key.equals(this.key);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return kind.getNamespace().hashCode() * 31 + key.hashCode();
+ }
}
-
+
/**
* Creates a new store instance that connects to Redis based on the provided {@link RedisFeatureStoreBuilder}.
*
@@ -151,11 +116,11 @@ private void createCache(long cacheTimeSecs, boolean refreshStaleValues, boolean
}
}
- private CacheLoader> createDefaultCacheLoader() {
- return new CacheLoader>() {
+ private CacheLoader> createDefaultCacheLoader() {
+ return new CacheLoader>() {
@Override
- public Optional load(String key) throws Exception {
- return Optional.fromNullable(getRedis(key));
+ public Optional load(CacheKey key) throws Exception {
+ return Optional.fromNullable(getRedis(key.kind, key.key));
}
};
}
@@ -171,7 +136,7 @@ private void createRefreshCache(long cacheTimeSecs, boolean asyncRefresh) {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(CACHE_REFRESH_THREAD_POOL_NAME_FORMAT).setDaemon(true).build();
ExecutorService parentExecutor = Executors.newSingleThreadExecutor(threadFactory);
executorService = MoreExecutors.listeningDecorator(parentExecutor);
- CacheLoader> cacheLoader = createDefaultCacheLoader();
+ CacheLoader> cacheLoader = createDefaultCacheLoader();
if (asyncRefresh) {
cacheLoader = CacheLoader.asyncReloading(cacheLoader, executorService);
}
@@ -197,72 +162,50 @@ public Boolean load(String key) throws Exception {
});
}
- /**
- * Returns the {@link FeatureFlag} to which the specified key is mapped, or
- * null if the key is not associated or the associated {@link FeatureFlag} has
- * been deleted.
- *
- * @param key the key whose associated {@link FeatureFlag} is to be returned
- * @return the {@link FeatureFlag} to which the specified key is mapped, or
- * null if the key is not associated or the associated {@link FeatureFlag} has
- * been deleted.
- */
+ @SuppressWarnings("unchecked")
@Override
- public FeatureFlag get(String key) {
- FeatureFlag featureFlag;
+ public T get(VersionedDataKind kind, String key) {
+ T item;
if (cache != null) {
- featureFlag = cache.getUnchecked(key).orNull();
+ item = (T) cache.getUnchecked(new CacheKey(kind, key)).orNull();
} else {
- featureFlag = getRedis(key);
+ item = getRedis(kind, key);
}
- if (featureFlag != null) {
- logger.debug("[get] Key: " + key + " with version: " + featureFlag.getVersion() + " found in feature store.");
+ if (item != null) {
+ logger.debug("[get] Key: {} with version: {} found in \"{}\".", key, item.getVersion(), kind.getNamespace());
}
- return featureFlag;
+ return item;
}
- /**
- * Returns a {@link java.util.Map} of all associated features. This implementation does not take advantage
- * of the in-memory cache, so fetching all features will involve a fetch from Redis.
- *
- * @return a map of all associated features.
- */
@Override
- public Map all() {
+ public Map all(VersionedDataKind kind) {
try (Jedis jedis = pool.getResource()) {
- Map featuresJson = jedis.hgetAll(featuresKey());
- Map result = new HashMap<>();
+ Map allJson = jedis.hgetAll(itemsKey(kind));
+ Map result = new HashMap<>();
Gson gson = new Gson();
- Type type = new TypeToken() {
- }.getType();
-
- for (Map.Entry entry : featuresJson.entrySet()) {
- FeatureFlag featureFlag = gson.fromJson(entry.getValue(), type);
- if (!featureFlag.isDeleted()) {
- result.put(entry.getKey(), featureFlag);
+ for (Map.Entry entry : allJson.entrySet()) {
+ T item = gson.fromJson(entry.getValue(), kind.getItemClass());
+ if (!item.isDeleted()) {
+ result.put(entry.getKey(), item);
}
}
return result;
}
}
- /**
- * Initializes (or re-initializes) the store with the specified set of features. Any existing entries
- * will be removed.
- *
- * @param features the features to set the store
- */
@Override
- public void init(Map features) {
+ public void init(Map, Map> allData) {
try (Jedis jedis = pool.getResource()) {
Gson gson = new Gson();
Transaction t = jedis.multi();
- t.del(featuresKey());
-
- for (FeatureFlag f : features.values()) {
- t.hset(featuresKey(), f.getKey(), gson.toJson(f));
+ 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(), gson.toJson(item));
+ }
}
t.exec();
@@ -271,36 +214,29 @@ public void init(Map features) {
initCache.put(INIT_KEY, true);
}
- /**
- * Deletes the feature associated with the specified key, if it exists and its version
- * is less than or equal to the specified version.
- *
- * @param key the key of the feature to be deleted
- * @param version the version for the delete operation
- */
@Override
- public void delete(String key, int version) {
+ public void delete(VersionedDataKind kind, String key, int version) {
Jedis jedis = null;
try {
Gson gson = new Gson();
jedis = pool.getResource();
- jedis.watch(featuresKey());
+ String baseKey = itemsKey(kind);
+ jedis.watch(baseKey);
- FeatureFlag feature = getRedis(key, jedis);
+ VersionedData item = getRedis(kind, key, jedis);
- if (feature != null && feature.getVersion() >= version) {
- logger.warn("Attempted to delete flag: " + key + " version: " + feature.getVersion() +
- " with a version that is the same or older: " + version);
+ if (item != null && item.getVersion() >= version) {
+ logger.warn("Attempted to delete key: {} version: {}" +
+ " with a version that is the same or older: {} in \"{}\"",
+ key, item.getVersion(), version, kind.getNamespace());
return;
}
- FeatureFlagBuilder newBuilder = new FeatureFlagBuilder(feature);
- newBuilder.deleted(true);
- newBuilder.version(version);
- jedis.hset(featuresKey(), key, gson.toJson(newBuilder.build()));
+ VersionedData deletedItem = kind.makeDeletedItem(key, version);
+ jedis.hset(baseKey, key, gson.toJson(deletedItem));
if (cache != null) {
- cache.invalidate(key);
+ cache.invalidate(new CacheKey(kind, key));
}
} finally {
if (jedis != null) {
@@ -310,33 +246,28 @@ public void delete(String key, int version) {
}
}
- /**
- * Update or insert the feature associated with the specified key, if its version
- * is less than or equal to the version specified in the argument feature.
- *
- * @param key
- * @param feature
- */
@Override
- public void upsert(String key, FeatureFlag feature) {
+ public void upsert(VersionedDataKind kind, T item) {
Jedis jedis = null;
try {
jedis = pool.getResource();
Gson gson = new Gson();
- jedis.watch(featuresKey());
+ String baseKey = itemsKey(kind);
+ jedis.watch(baseKey);
- FeatureFlag f = getRedisEvenIfDeleted(key, jedis);
+ VersionedData old = getRedisEvenIfDeleted(kind, item.getKey(), jedis);
- if (f != null && f.getVersion() >= feature.getVersion()) {
- logger.warn("Attempted to update flag: " + key + " version: " + f.getVersion() +
- " with a version that is the same or older: " + feature.getVersion());
+ if (old != null && old.getVersion() >= item.getVersion()) {
+ logger.warn("Attempted to update key: {} version: {}" +
+ " with a version that is the same or older: {} in \"{}\"",
+ item.getKey(), old.getVersion(), item.getVersion(), kind.getNamespace());
return;
}
- jedis.hset(featuresKey(), key, gson.toJson(feature));
+ jedis.hset(baseKey, item.getKey(), gson.toJson(item));
if (cache != null) {
- cache.invalidate(key);
+ cache.invalidate(new CacheKey(kind, item.getKey()));
}
} finally {
if (jedis != null) {
@@ -346,11 +277,6 @@ public void upsert(String key, FeatureFlag feature) {
}
}
- /**
- * Returns true if this store has been initialized
- *
- * @return true if this store has been initialized
- */
@Override
public boolean initialized() {
// The LoadingCache takes care of both coalescing multiple simultaneous requests and memoizing
@@ -362,7 +288,7 @@ public boolean initialized() {
/**
* Releases all resources associated with the store. The store must no longer be used once closed.
*
- * @throws IOException
+ * @throws IOException if an underlying service threw an exception
*/
public void close() throws IOException {
logger.info("Closing LaunchDarkly RedisFeatureStore");
@@ -387,41 +313,41 @@ public CacheStats getCacheStats() {
return null;
}
- private String featuresKey() {
- return prefix + ":features";
+ private String itemsKey(VersionedDataKind> kind) {
+ return prefix + ":" + kind.getNamespace();
}
private Boolean getInit() {
try (Jedis jedis = pool.getResource()) {
- return jedis.exists(featuresKey());
+ return jedis.exists(itemsKey(FEATURES));
}
}
- private FeatureFlag getRedis(String key) {
+ private T getRedis(VersionedDataKind kind, String key) {
try (Jedis jedis = pool.getResource()) {
- return getRedis(key, jedis);
+ return getRedis(kind, key, jedis);
}
}
- private FeatureFlag getRedis(String key, Jedis jedis) {
- FeatureFlag f = getRedisEvenIfDeleted(key, jedis);
- if (f != null && f.isDeleted()) {
- logger.debug("[get] Key: " + key + " has been deleted. Returning null");
+ private T getRedis(VersionedDataKind kind, String key, Jedis jedis) {
+ T item = getRedisEvenIfDeleted(kind, key, jedis);
+ if (item != null && item.isDeleted()) {
+ logger.debug("[get] Key: {} has been deleted in \"{}\". Returning null", key, kind.getNamespace());
return null;
}
- return f;
+ return item;
}
- private FeatureFlag getRedisEvenIfDeleted(String key, Jedis jedis) {
+ private T getRedisEvenIfDeleted(VersionedDataKind kind, String key, Jedis jedis) {
Gson gson = new Gson();
- String featureJson = jedis.hget(featuresKey(), key);
+ String json = jedis.hget(itemsKey(kind), key);
- if (featureJson == null) {
- logger.debug("[get] Key: " + key + " not found in feature store. Returning null");
+ if (json == null) {
+ logger.debug("[get] Key: {} not found in \"{}\". Returning null", key, kind.getNamespace());
return null;
}
- return gson.fromJson(featureJson, FeatureFlag.class);
+ return gson.fromJson(json, kind.getItemClass());
}
private static JedisPoolConfig getPoolConfig() {
diff --git a/src/main/java/com/launchdarkly/client/RedisFeatureStoreBuilder.java b/src/main/java/com/launchdarkly/client/RedisFeatureStoreBuilder.java
index 2d75b2686..ebebffbf8 100644
--- a/src/main/java/com/launchdarkly/client/RedisFeatureStoreBuilder.java
+++ b/src/main/java/com/launchdarkly/client/RedisFeatureStoreBuilder.java
@@ -52,7 +52,7 @@ public RedisFeatureStoreBuilder(URI uri, long cacheTimeSecs) {
* @param host the hostname to connect to
* @param port the port to connect to
* @param cacheTimeSecs the cache time in seconds. See {@link RedisFeatureStoreBuilder#cacheTime(long, TimeUnit)} for more information.
- * @throws URISyntaxException
+ * @throws URISyntaxException if the URI is not valid
*/
public RedisFeatureStoreBuilder(String scheme, String host, int port, long cacheTimeSecs) throws URISyntaxException {
this.uri = new URI(scheme, null, host, port, null, null, null);
@@ -110,7 +110,7 @@ public RedisFeatureStoreBuilder asyncRefresh(boolean enabled) {
/**
* Optionally configures the namespace prefix for all keys stored in Redis.
*
- * @param prefix
+ * @param prefix the namespace prefix
* @return the builder
*/
public RedisFeatureStoreBuilder prefix(String prefix) {
diff --git a/src/main/java/com/launchdarkly/client/Rule.java b/src/main/java/com/launchdarkly/client/Rule.java
index f240a9741..6009abc74 100644
--- a/src/main/java/com/launchdarkly/client/Rule.java
+++ b/src/main/java/com/launchdarkly/client/Rule.java
@@ -20,12 +20,12 @@ class Rule extends VariationOrRollout {
this.clauses = clauses;
}
- boolean matchesUser(LDUser user) {
+ boolean matchesUser(FeatureStore store, LDUser user) {
for (Clause clause : clauses) {
- if (!clause.matchesUser(user)) {
+ if (!clause.matchesUser(store, user)) {
return false;
}
}
return true;
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/launchdarkly/client/Segment.java b/src/main/java/com/launchdarkly/client/Segment.java
new file mode 100644
index 000000000..eb7fc24d8
--- /dev/null
+++ b/src/main/java/com/launchdarkly/client/Segment.java
@@ -0,0 +1,148 @@
+package com.launchdarkly.client;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gson.reflect.TypeToken;
+
+class Segment implements VersionedData {
+
+ private static final Type mapType = new TypeToken