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

Commit 35696c7

Browse files
committed
Merge pull request #49 from ivorp2/ivorp/redis-lazy-refresh
RedisFeatureStore config and functionality additions
2 parents 868d92a + 29cf485 commit 35696c7

File tree

3 files changed

+364
-18
lines changed

3 files changed

+364
-18
lines changed

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

Lines changed: 98 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,9 @@ 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 1.1. Please use the {@link RedisFeatureStoreBuilder#build()} for a more flexible way of constructing a {@link RedisFeatureStore}.
4151
*/
52+
@Deprecated
4253
public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSecs) {
4354
this(host, port, prefix, cacheTimeSecs, getPoolConfig());
4455
}
@@ -50,20 +61,24 @@ public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSec
5061
* @param uri the URI for the Redis connection
5162
* @param prefix a namespace prefix for all keys stored in Redis
5263
* @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
64+
* @deprecated as of 1.1. Please use the {@link RedisFeatureStoreBuilder#build()} for a more flexible way of constructing a {@link RedisFeatureStore}.
5365
*/
66+
@Deprecated
5467
public RedisFeatureStore(URI uri, String prefix, long cacheTimeSecs) {
5568
this(uri, prefix, cacheTimeSecs, getPoolConfig());
5669
}
5770

5871
/**
59-
* Creates a new store instance that connects to Redis with the provided URI, prefix, cache timeout, and connection pool settings.
72+
* Creates a new store instance that connects to Redis with the provided host, port, prefix, cache timeout, and connection pool settings.
6073
*
6174
* @param host the host for the Redis connection
6275
* @param port the port for the Redis connection
6376
* @param prefix a namespace prefix for all keys stored in Redis
6477
* @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
6578
* @param poolConfig an optional pool config for the Jedis connection pool
79+
* @deprecated as of 1.1. Please use the {@link RedisFeatureStoreBuilder#build()} for a more flexible way of constructing a {@link RedisFeatureStore}.
6680
*/
81+
@Deprecated
6782
public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSecs, JedisPoolConfig poolConfig) {
6883
pool = new JedisPool(poolConfig, host, port);
6984
setPrefix(prefix);
@@ -78,14 +93,34 @@ public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSec
7893
* @param prefix a namespace prefix for all keys stored in Redis
7994
* @param cacheTimeSecs an optional timeout for the in-memory cache. If set to 0, no in-memory caching will be performed
8095
* @param poolConfig an optional pool config for the Jedis connection pool
96+
* @deprecated as of 1.1. Please use the {@link RedisFeatureStoreBuilder#build()} for a more flexible way of constructing a {@link RedisFeatureStore}.
8197
*/
98+
@Deprecated
8299
public RedisFeatureStore(URI uri, String prefix, long cacheTimeSecs, JedisPoolConfig poolConfig) {
83100
pool = new JedisPool(poolConfig, uri);
84101
setPrefix(prefix);
85102
createCache(cacheTimeSecs);
86103
createInitCache(cacheTimeSecs);
87104
}
88105

106+
/**
107+
* Creates a new store instance that connects to Redis based on the provided {@link RedisFeatureStoreBuilder}.
108+
*
109+
* See the {@link RedisFeatureStoreBuilder} for information on available configuration options and what they do.
110+
*
111+
* @param builder the configured builder to construct the store with.
112+
*/
113+
protected RedisFeatureStore(RedisFeatureStoreBuilder builder) {
114+
if (builder.poolConfig == null) {
115+
this.pool = new JedisPool(getPoolConfig(), builder.uri, builder.connectTimeout, builder.socketTimeout);
116+
} else {
117+
this.pool = new JedisPool(builder.poolConfig, builder.uri, builder.connectTimeout, builder.socketTimeout);
118+
}
119+
setPrefix(builder.prefix);
120+
createCache(builder.cacheTimeSecs, builder.refreshStaleValues, builder.asyncRefresh);
121+
createInitCache(builder.cacheTimeSecs);
122+
}
123+
89124
/**
90125
* Creates a new store instance that connects to Redis with a default connection (localhost port 6379) and no in-memory cache.
91126
*
@@ -95,7 +130,6 @@ public RedisFeatureStore() {
95130
this.prefix = DEFAULT_PREFIX;
96131
}
97132

98-
99133
private void setPrefix(String prefix) {
100134
if (prefix == null || prefix.isEmpty()) {
101135
this.prefix = DEFAULT_PREFIX;
@@ -105,21 +139,56 @@ private void setPrefix(String prefix) {
105139
}
106140

107141
private void createCache(long cacheTimeSecs) {
142+
createCache(cacheTimeSecs, false, false);
143+
}
144+
145+
private void createCache(long cacheTimeSecs, boolean refreshStaleValues, boolean asyncRefresh) {
108146
if (cacheTimeSecs > 0) {
109-
cache = CacheBuilder.newBuilder().expireAfterWrite(cacheTimeSecs, TimeUnit.SECONDS).build(new CacheLoader<String, FeatureRep<?>>() {
147+
if (refreshStaleValues) {
148+
createRefreshCache(cacheTimeSecs, asyncRefresh);
149+
} else {
150+
createExpiringCache(cacheTimeSecs);
151+
}
152+
}
153+
}
110154

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

119189
private void createInitCache(long cacheTimeSecs) {
120190
if (cacheTimeSecs > 0) {
121191
initCache = CacheBuilder.newBuilder().expireAfterWrite(cacheTimeSecs, TimeUnit.SECONDS).build(new CacheLoader<String, Boolean>() {
122-
123192
@Override
124193
public Boolean load(String key) throws Exception {
125194
return getInit();
@@ -129,7 +198,6 @@ public Boolean load(String key) throws Exception {
129198
}
130199

131200
/**
132-
*
133201
* Returns the {@link com.launchdarkly.client.FeatureRep} to which the specified key is mapped, or
134202
* null if the key is not associated or the associated {@link com.launchdarkly.client.FeatureRep} has
135203
* been deleted.
@@ -148,12 +216,10 @@ public FeatureRep<?> get(String key) {
148216
}
149217
}
150218

151-
152219
/**
153220
* Returns a {@link java.util.Map} of all associated features. This implementation does not take advantage
154221
* of the in-memory cache, so fetching all features will involve a fetch from Redis.
155222
*
156-
*
157223
* @return a map of all associated features.
158224
*/
159225
@Override
@@ -171,8 +237,8 @@ public Map<String, FeatureRep<?>> all() {
171237
}
172238
return result;
173239
}
174-
175240
}
241+
176242
/**
177243
* Initializes (or re-initializes) the store with the specified set of features. Any existing entries
178244
* will be removed.
@@ -195,9 +261,7 @@ public void init(Map<String, FeatureRep<?>> features) {
195261
}
196262
}
197263

198-
199264
/**
200-
*
201265
* Deletes the feature associated with the specified key, if it exists and its version
202266
* is less than or equal to the specified version.
203267
*
@@ -278,10 +342,26 @@ public boolean initialized() {
278342
*/
279343
public void close() throws IOException
280344
{
281-
pool.destroy();
345+
try {
346+
if (executorService != null) {
347+
executorService.shutdownNow();
348+
}
349+
} finally {
350+
pool.destroy();
351+
}
282352
}
283353

284-
354+
/**
355+
* Return the underlying Guava cache stats object.
356+
*
357+
* @return the cache statistics object.
358+
*/
359+
public CacheStats getCacheStats() {
360+
if (cache != null) {
361+
return cache.stats();
362+
}
363+
return null;
364+
}
285365

286366
private String featuresKey() {
287367
return prefix + ":features";

0 commit comments

Comments
 (0)