22
33import com .google .common .cache .CacheBuilder ;
44import com .google .common .cache .CacheLoader ;
5+ import com .google .common .cache .CacheStats ;
56import 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 ;
610import com .google .gson .Gson ;
711import com .google .gson .reflect .TypeToken ;
812import redis .clients .jedis .Jedis ;
1519import java .net .URI ;
1620import java .util .HashMap ;
1721import java .util .Map ;
22+ import java .util .concurrent .ExecutorService ;
23+ import java .util .concurrent .Executors ;
24+ import java .util .concurrent .ThreadFactory ;
1825import java .util .concurrent .TimeUnit ;
1926
2027/**
2431 */
2532public 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