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

Commit dbca075

Browse files
committed
Added:
- Configuration options to set the Jedis pool connection and socket timeouts in the RedisFeatureStore. - Support for 3 modes of RedisFeatureStore local cache behaviour; default evict, refresh and async refresh. - A RedisFeatureStoreBuilder with it's own builder. Deprecated the old constructors for the RedisFeatureStore. - Tests for the RedisFeatureStoreBuilder.
1 parent 876e1d3 commit dbca075

File tree

3 files changed

+368
-18
lines changed

3 files changed

+368
-18
lines changed

src/main/java/com/launchdarkly/client/RedisFeatureStore.java

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
import com.google.common.cache.CacheBuilder;
44
import com.google.common.cache.CacheLoader;
5+
import com.google.common.cache.CacheStats;
56
import com.google.common.cache.LoadingCache;
7+
import com.google.common.util.concurrent.ListeningExecutorService;
8+
import com.google.common.util.concurrent.MoreExecutors;
9+
import com.google.common.util.concurrent.ThreadFactoryBuilder;
610
import com.google.gson.Gson;
711
import com.google.gson.reflect.TypeToken;
812
import redis.clients.jedis.Jedis;
@@ -15,6 +19,9 @@
1519
import java.net.URI;
1620
import java.util.HashMap;
1721
import java.util.Map;
22+
import java.util.concurrent.ExecutorService;
23+
import java.util.concurrent.Executors;
24+
import java.util.concurrent.ThreadFactory;
1825
import java.util.concurrent.TimeUnit;
1926

2027
/**
@@ -24,11 +31,13 @@
2431
*/
2532
public class RedisFeatureStore implements FeatureStore {
2633
private static final String DEFAULT_PREFIX = "launchdarkly";
34+
private static final String INIT_KEY = "$initialized$";
35+
private static final String CACHE_REFRESH_THREAD_POOL_NAME_FORMAT = "RedisFeatureStore-cache-refresher-pool-%d";
2736
private final JedisPool pool;
2837
private LoadingCache<String, FeatureRep<?>> cache;
2938
private LoadingCache<String, Boolean> initCache;
3039
private String prefix;
31-
private static final String INIT_KEY = "$initialized$";
40+
private ListeningExecutorService executorService;
3241

3342
/**
3443
* Creates a new store instance that connects to Redis with the provided host, port, prefix, and cache timeout. Uses a default
@@ -38,7 +47,10 @@ public class RedisFeatureStore implements FeatureStore {
3847
* @param port the port for the Redis connection
3948
* @param prefix a namespace prefix for all keys stored in Redis
4049
* @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
50+
* @deprecated as of X. Please use the {@link com.launchdarkly.client.RedisFeatureStore#RedisFeatureStore(RedisFeatureStoreBuilder)} constructor for a more
51+
* flexible way of creating the FeatureStore.
4152
*/
53+
@Deprecated
4254
public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSecs) {
4355
this(host, port, prefix, cacheTimeSecs, getPoolConfig());
4456
}
@@ -50,20 +62,26 @@ public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSec
5062
* @param uri the URI for the Redis connection
5163
* @param prefix a namespace prefix for all keys stored in Redis
5264
* @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
65+
* @deprecated as of X. Please use the {@link com.launchdarkly.client.RedisFeatureStore#RedisFeatureStore(RedisFeatureStoreBuilder)} constructor for a more
66+
* flexible way of creating the FeatureStore.
5367
*/
68+
@Deprecated
5469
public RedisFeatureStore(URI uri, String prefix, long cacheTimeSecs) {
5570
this(uri, prefix, cacheTimeSecs, getPoolConfig());
5671
}
5772

5873
/**
59-
* Creates a new store instance that connects to Redis with the provided URI, prefix, cache timeout, and connection pool settings.
74+
* Creates a new store instance that connects to Redis with the provided host, port, prefix, cache timeout, and connection pool settings.
6075
*
6176
* @param host the host for the Redis connection
6277
* @param port the port for the Redis connection
6378
* @param prefix a namespace prefix for all keys stored in Redis
6479
* @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
6580
* @param poolConfig an optional pool config for the Jedis connection pool
81+
* @deprecated as of X. Please use the {@link com.launchdarkly.client.RedisFeatureStore#RedisFeatureStore(RedisFeatureStoreBuilder)} constructor for a more
82+
* flexible way of creating the FeatureStore.
6683
*/
84+
@Deprecated
6785
public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSecs, JedisPoolConfig poolConfig) {
6886
pool = new JedisPool(poolConfig, host, port);
6987
setPrefix(prefix);
@@ -78,14 +96,35 @@ public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSec
7896
* @param prefix a namespace prefix for all keys stored in Redis
7997
* @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
8098
* @param poolConfig an optional pool config for the Jedis connection pool
99+
* @deprecated as of X. Please use the {@link com.launchdarkly.client.RedisFeatureStore#RedisFeatureStore(RedisFeatureStoreBuilder)} constructor for a more
100+
* flexible way of creating the FeatureStore.
81101
*/
102+
@Deprecated
82103
public RedisFeatureStore(URI uri, String prefix, long cacheTimeSecs, JedisPoolConfig poolConfig) {
83104
pool = new JedisPool(poolConfig, uri);
84105
setPrefix(prefix);
85106
createCache(cacheTimeSecs);
86107
createInitCache(cacheTimeSecs);
87108
}
88109

110+
/**
111+
* Creates a new store instance that connects to Redis based on the provided {@link RedisFeatureStoreBuilder}.
112+
*
113+
* See the {@link RedisFeatureStoreBuilder} for information on available configuration options and what they do.
114+
*
115+
* @param builder the store configuration
116+
*/
117+
public RedisFeatureStore(RedisFeatureStoreBuilder builder) {
118+
if (builder.poolConfig == null) {
119+
this.pool = new JedisPool(getPoolConfig(), builder.uri, builder.connectTimeout, builder.socketTimeout);
120+
} else {
121+
this.pool = new JedisPool(builder.poolConfig, builder.uri, builder.connectTimeout, builder.socketTimeout);
122+
}
123+
setPrefix(builder.prefix);
124+
createCache(builder.cacheTimeSecs, builder.refreshStaleValues, builder.asyncRefresh);
125+
createInitCache(builder.cacheTimeSecs);
126+
}
127+
89128
/**
90129
* Creates a new store instance that connects to Redis with a default connection (localhost port 6379) and no in-memory cache.
91130
*
@@ -95,7 +134,6 @@ public RedisFeatureStore() {
95134
this.prefix = DEFAULT_PREFIX;
96135
}
97136

98-
99137
private void setPrefix(String prefix) {
100138
if (prefix == null || prefix.isEmpty()) {
101139
this.prefix = DEFAULT_PREFIX;
@@ -105,21 +143,56 @@ private void setPrefix(String prefix) {
105143
}
106144

107145
private void createCache(long cacheTimeSecs) {
146+
createCache(cacheTimeSecs, false, false);
147+
}
148+
149+
private void createCache(long cacheTimeSecs, boolean refreshStaleValues, boolean asyncRefresh) {
108150
if (cacheTimeSecs > 0) {
109-
cache = CacheBuilder.newBuilder().expireAfterWrite(cacheTimeSecs, TimeUnit.SECONDS).build(new CacheLoader<String, FeatureRep<?>>() {
151+
if (refreshStaleValues) {
152+
createRefreshCache(cacheTimeSecs, asyncRefresh);
153+
} else {
154+
createExpiringCache(cacheTimeSecs);
155+
}
156+
}
157+
}
110158

111-
@Override
112-
public FeatureRep<?> load(String key) throws Exception {
113-
return getRedis(key);
114-
}
115-
});
159+
private CacheLoader<String, FeatureRep<?>> createDefaultCacheLoader() {
160+
return new CacheLoader<String, FeatureRep<?>>() {
161+
@Override
162+
public FeatureRep<?> load(String key) throws Exception {
163+
return getRedis(key);
164+
}
165+
};
166+
}
167+
168+
/**
169+
* Configures the instance to use a "refresh after write" cache. This will not automatically evict stale values, allowing them to be returned if failures
170+
* occur when updating them. Optionally set the cache to refresh values asynchronously, which always returns the previously cached value immediately.
171+
* @param cacheTimeSecs the length of time in seconds, after a {@link FeatureRep} value is created that it should be refreshed.
172+
* @param asyncRefresh makes the refresh asynchronous or not.
173+
*/
174+
private void createRefreshCache(long cacheTimeSecs, boolean asyncRefresh) {
175+
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(CACHE_REFRESH_THREAD_POOL_NAME_FORMAT).setDaemon(true).build();
176+
ExecutorService parentExecutor = Executors.newSingleThreadExecutor(threadFactory);
177+
executorService = MoreExecutors.listeningDecorator(parentExecutor);
178+
CacheLoader<String, FeatureRep<?>> cacheLoader = createDefaultCacheLoader();
179+
if (asyncRefresh) {
180+
cacheLoader = CacheLoader.asyncReloading(cacheLoader, executorService);
116181
}
182+
cache = CacheBuilder.newBuilder().refreshAfterWrite(cacheTimeSecs, TimeUnit.SECONDS).build(cacheLoader);
183+
}
184+
185+
/**
186+
* Configures the instance to use an "expire after write" cache. This will evict stale values and block while loading the latest from Redis.
187+
* @param cacheTimeSecs the length of time in seconds, after a {@link FeatureRep} value is created that it should be automatically removed.
188+
*/
189+
private void createExpiringCache(long cacheTimeSecs) {
190+
cache = CacheBuilder.newBuilder().expireAfterWrite(cacheTimeSecs, TimeUnit.SECONDS).build(createDefaultCacheLoader());
117191
}
118192

119193
private void createInitCache(long cacheTimeSecs) {
120194
if (cacheTimeSecs > 0) {
121195
initCache = CacheBuilder.newBuilder().expireAfterWrite(cacheTimeSecs, TimeUnit.SECONDS).build(new CacheLoader<String, Boolean>() {
122-
123196
@Override
124197
public Boolean load(String key) throws Exception {
125198
return getInit();
@@ -129,7 +202,6 @@ public Boolean load(String key) throws Exception {
129202
}
130203

131204
/**
132-
*
133205
* Returns the {@link com.launchdarkly.client.FeatureRep} to which the specified key is mapped, or
134206
* null if the key is not associated or the associated {@link com.launchdarkly.client.FeatureRep} has
135207
* been deleted.
@@ -148,12 +220,10 @@ public FeatureRep<?> get(String key) {
148220
}
149221
}
150222

151-
152223
/**
153224
* Returns a {@link java.util.Map} of all associated features. This implementation does not take advantage
154225
* of the in-memory cache, so fetching all features will involve a fetch from Redis.
155226
*
156-
*
157227
* @return a map of all associated features.
158228
*/
159229
@Override
@@ -171,8 +241,8 @@ public Map<String, FeatureRep<?>> all() {
171241
}
172242
return result;
173243
}
174-
175244
}
245+
176246
/**
177247
* Initializes (or re-initializes) the store with the specified set of features. Any existing entries
178248
* will be removed.
@@ -195,9 +265,7 @@ public void init(Map<String, FeatureRep<?>> features) {
195265
}
196266
}
197267

198-
199268
/**
200-
*
201269
* Deletes the feature associated with the specified key, if it exists and its version
202270
* is less than or equal to the specified version.
203271
*
@@ -278,10 +346,26 @@ public boolean initialized() {
278346
*/
279347
public void close() throws IOException
280348
{
281-
pool.destroy();
349+
try {
350+
if (executorService != null) {
351+
executorService.shutdownNow();
352+
}
353+
} finally {
354+
pool.destroy();
355+
}
282356
}
283357

284-
358+
/**
359+
* Return the underlying Guava cache stats object.
360+
*
361+
* @return the cache statistics object.
362+
*/
363+
public CacheStats getCacheStats() {
364+
if (cache != null) {
365+
return cache.stats();
366+
}
367+
return null;
368+
}
285369

286370
private String featuresKey() {
287371
return prefix + ":features";

0 commit comments

Comments
 (0)