1
1
package com .launchdarkly .client ;
2
2
3
- import static com .launchdarkly .client .VersionedDataKind .FEATURES ;
4
-
5
- import java .io .IOException ;
6
- import java .net .URI ;
7
- import java .util .HashMap ;
8
- import java .util .Map ;
9
- import java .util .concurrent .ExecutorService ;
10
- import java .util .concurrent .Executors ;
11
- import java .util .concurrent .ThreadFactory ;
12
- import java .util .concurrent .TimeUnit ;
13
-
14
- import org .slf4j .Logger ;
15
- import org .slf4j .LoggerFactory ;
16
-
3
+ import com .google .common .annotations .VisibleForTesting ;
17
4
import com .google .common .base .Optional ;
18
5
import com .google .common .cache .CacheBuilder ;
19
6
import com .google .common .cache .CacheLoader ;
24
11
import com .google .common .util .concurrent .ThreadFactoryBuilder ;
25
12
import com .google .gson .Gson ;
26
13
14
+ import org .slf4j .Logger ;
15
+ import org .slf4j .LoggerFactory ;
16
+
17
+ import java .io .IOException ;
18
+ import java .util .HashMap ;
19
+ import java .util .List ;
20
+ import java .util .Map ;
21
+ import java .util .concurrent .ExecutorService ;
22
+ import java .util .concurrent .Executors ;
23
+ import java .util .concurrent .ThreadFactory ;
24
+ import java .util .concurrent .TimeUnit ;
25
+
26
+ import static com .launchdarkly .client .VersionedDataKind .FEATURES ;
27
+
27
28
import redis .clients .jedis .Jedis ;
28
29
import redis .clients .jedis .JedisPool ;
29
30
import redis .clients .jedis .JedisPoolConfig ;
@@ -38,12 +39,15 @@ public class RedisFeatureStore implements FeatureStore {
38
39
private static final String DEFAULT_PREFIX = "launchdarkly" ;
39
40
private static final String INIT_KEY = "$initialized$" ;
40
41
private static final String CACHE_REFRESH_THREAD_POOL_NAME_FORMAT = "RedisFeatureStore-cache-refresher-pool-%d" ;
42
+ private static final Gson gson = new Gson ();
43
+
41
44
private final JedisPool pool ;
42
45
private LoadingCache <CacheKey , Optional <VersionedData >> cache ;
43
46
private final LoadingCache <String , Boolean > initCache = createInitCache ();
44
47
private String prefix ;
45
48
private ListeningExecutorService executorService ;
46
-
49
+ private UpdateListener updateListener ;
50
+
47
51
private static class CacheKey {
48
52
final VersionedDataKind <?> kind ;
49
53
final String key ;
@@ -102,10 +106,6 @@ private void setPrefix(String prefix) {
102
106
}
103
107
}
104
108
105
- private void createCache (long cacheTimeSecs ) {
106
- createCache (cacheTimeSecs , false , false );
107
- }
108
-
109
109
private void createCache (long cacheTimeSecs , boolean refreshStaleValues , boolean asyncRefresh ) {
110
110
if (cacheTimeSecs > 0 ) {
111
111
if (refreshStaleValues ) {
@@ -120,7 +120,9 @@ private CacheLoader<CacheKey, Optional<VersionedData>> createDefaultCacheLoader(
120
120
return new CacheLoader <CacheKey , Optional <VersionedData >>() {
121
121
@ Override
122
122
public Optional <VersionedData > load (CacheKey key ) throws Exception {
123
- return Optional .<VersionedData >fromNullable (getRedis (key .kind , key .key ));
123
+ try (Jedis jedis = pool .getResource ()) {
124
+ return Optional .<VersionedData >fromNullable (getRedisEvenIfDeleted (key .kind , key .key , jedis ));
125
+ }
124
126
}
125
127
};
126
128
}
@@ -169,7 +171,13 @@ public <T extends VersionedData> T get(VersionedDataKind<T> kind, String key) {
169
171
if (cache != null ) {
170
172
item = (T ) cache .getUnchecked (new CacheKey (kind , key )).orNull ();
171
173
} else {
172
- item = getRedis (kind , key );
174
+ try (Jedis jedis = pool .getResource ()) {
175
+ item = getRedisEvenIfDeleted (kind , key , jedis );
176
+ }
177
+ }
178
+ if (item != null && item .isDeleted ()) {
179
+ logger .debug ("[get] Key: {} has been deleted in \" {}\" . Returning null" , key , kind .getNamespace ());
180
+ return null ;
173
181
}
174
182
if (item != null ) {
175
183
logger .debug ("[get] Key: {} with version: {} found in \" {}\" ." , key , item .getVersion (), kind .getNamespace ());
@@ -182,7 +190,6 @@ public <T extends VersionedData> Map<String, T> all(VersionedDataKind<T> kind) {
182
190
try (Jedis jedis = pool .getResource ()) {
183
191
Map <String , String > allJson = jedis .hgetAll (itemsKey (kind ));
184
192
Map <String , T > result = new HashMap <>();
185
- Gson gson = new Gson ();
186
193
187
194
for (Map .Entry <String , String > entry : allJson .entrySet ()) {
188
195
T item = gson .fromJson (entry .getValue (), kind .getItemClass ());
@@ -197,7 +204,6 @@ public <T extends VersionedData> Map<String, T> all(VersionedDataKind<T> kind) {
197
204
@ Override
198
205
public void init (Map <VersionedDataKind <?>, Map <String , ? extends VersionedData >> allData ) {
199
206
try (Jedis jedis = pool .getResource ()) {
200
- Gson gson = new Gson ();
201
207
Transaction t = jedis .multi ();
202
208
203
209
for (Map .Entry <VersionedDataKind <?>, Map <String , ? extends VersionedData >> entry : allData .entrySet ()) {
@@ -216,63 +222,54 @@ public void init(Map<VersionedDataKind<?>, Map<String, ? extends VersionedData>>
216
222
217
223
@ Override
218
224
public <T extends VersionedData > void delete (VersionedDataKind <T > kind , String key , int version ) {
219
- Jedis jedis = null ;
220
- try {
221
- Gson gson = new Gson ();
222
- jedis = pool .getResource ();
223
- String baseKey = itemsKey (kind );
224
- jedis .watch (baseKey );
225
-
226
- VersionedData item = getRedis (kind , key , jedis );
227
-
228
- if (item != null && item .getVersion () >= version ) {
229
- logger .warn ("Attempted to delete key: {} version: {}" +
230
- " with a version that is the same or older: {} in \" {}\" " ,
231
- key , item .getVersion (), version , kind .getNamespace ());
232
- return ;
233
- }
234
-
235
- VersionedData deletedItem = kind .makeDeletedItem (key , version );
236
- jedis .hset (baseKey , key , gson .toJson (deletedItem ));
237
-
238
- if (cache != null ) {
239
- cache .invalidate (new CacheKey (kind , key ));
240
- }
241
- } finally {
242
- if (jedis != null ) {
243
- jedis .unwatch ();
244
- jedis .close ();
245
- }
246
- }
225
+ T deletedItem = kind .makeDeletedItem (key , version );
226
+ updateItemWithVersioning (kind , deletedItem );
247
227
}
248
-
228
+
249
229
@ Override
250
230
public <T extends VersionedData > void upsert (VersionedDataKind <T > kind , T item ) {
251
- Jedis jedis = null ;
252
- try {
253
- jedis = pool .getResource ();
254
- Gson gson = new Gson ();
255
- String baseKey = itemsKey (kind );
256
- jedis .watch (baseKey );
257
-
258
- VersionedData old = getRedisEvenIfDeleted (kind , item .getKey (), jedis );
259
-
260
- if (old != null && old .getVersion () >= item .getVersion ()) {
261
- logger .warn ("Attempted to update key: {} version: {}" +
262
- " with a version that is the same or older: {} in \" {}\" " ,
263
- item .getKey (), old .getVersion (), item .getVersion (), kind .getNamespace ());
264
- return ;
265
- }
266
-
267
- jedis .hset (baseKey , item .getKey (), gson .toJson (item ));
231
+ updateItemWithVersioning (kind , item );
232
+ }
268
233
269
- if (cache != null ) {
270
- cache .invalidate (new CacheKey (kind , item .getKey ()));
271
- }
272
- } finally {
273
- if (jedis != null ) {
274
- jedis .unwatch ();
275
- jedis .close ();
234
+ private <T extends VersionedData > void updateItemWithVersioning (VersionedDataKind <T > kind , T newItem ) {
235
+ while (true ) {
236
+ Jedis jedis = null ;
237
+ try {
238
+ jedis = pool .getResource ();
239
+ String baseKey = itemsKey (kind );
240
+ jedis .watch (baseKey );
241
+
242
+ if (updateListener != null ) {
243
+ updateListener .aboutToUpdate (baseKey , newItem .getKey ());
244
+ }
245
+
246
+ VersionedData oldItem = getRedisEvenIfDeleted (kind , newItem .getKey (), jedis );
247
+
248
+ if (oldItem != null && oldItem .getVersion () >= newItem .getVersion ()) {
249
+ logger .warn ("Attempted to {} key: {} version: {}" +
250
+ " with a version that is the same or older: {} in \" {}\" " ,
251
+ newItem .isDeleted () ? "delete" : "update" ,
252
+ newItem .getKey (), oldItem .getVersion (), newItem .getVersion (), kind .getNamespace ());
253
+ return ;
254
+ }
255
+
256
+ Transaction tx = jedis .multi ();
257
+ tx .hset (baseKey , newItem .getKey (), gson .toJson (newItem ));
258
+ List <Object > result = tx .exec ();
259
+ if (result .isEmpty ()) {
260
+ // if exec failed, it means the watch was triggered and we should retry
261
+ logger .debug ("Concurrent modification detected, retrying" );
262
+ continue ;
263
+ }
264
+
265
+ if (cache != null ) {
266
+ cache .invalidate (new CacheKey (kind , newItem .getKey ()));
267
+ }
268
+ } finally {
269
+ if (jedis != null ) {
270
+ jedis .unwatch ();
271
+ jedis .close ();
272
+ }
276
273
}
277
274
}
278
275
}
@@ -323,23 +320,7 @@ private Boolean getInit() {
323
320
}
324
321
}
325
322
326
- private <T extends VersionedData > T getRedis (VersionedDataKind <T > kind , String key ) {
327
- try (Jedis jedis = pool .getResource ()) {
328
- return getRedis (kind , key , jedis );
329
- }
330
- }
331
-
332
- private <T extends VersionedData > T getRedis (VersionedDataKind <T > kind , String key , Jedis jedis ) {
333
- T item = getRedisEvenIfDeleted (kind , key , jedis );
334
- if (item != null && item .isDeleted ()) {
335
- logger .debug ("[get] Key: {} has been deleted in \" {}\" . Returning null" , key , kind .getNamespace ());
336
- return null ;
337
- }
338
- return item ;
339
- }
340
-
341
323
private <T extends VersionedData > T getRedisEvenIfDeleted (VersionedDataKind <T > kind , String key , Jedis jedis ) {
342
- Gson gson = new Gson ();
343
324
String json = jedis .hget (itemsKey (kind ), key );
344
325
345
326
if (json == null ) {
@@ -354,4 +335,12 @@ private static JedisPoolConfig getPoolConfig() {
354
335
return new JedisPoolConfig ();
355
336
}
356
337
338
+ static interface UpdateListener {
339
+ void aboutToUpdate (String baseKey , String itemKey );
340
+ }
341
+
342
+ @ VisibleForTesting
343
+ void setUpdateListener (UpdateListener updateListener ) {
344
+ this .updateListener = updateListener ;
345
+ }
357
346
}
0 commit comments