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

Commit 0602c8e

Browse files
committed
Merge pull request #30 from launchdarkly/jko/redis-store
Support for optional Redis-backed feature store
2 parents bc4abd9 + 692d687 commit 0602c8e

File tree

6 files changed

+190
-6
lines changed

6 files changed

+190
-6
lines changed

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ repositories {
1111

1212
allprojects {
1313
group = 'com.launchdarkly'
14-
version = "0.16.0"
14+
version = "0.17.3-SNAPSHOT"
1515
sourceCompatibility = 1.6
1616
targetCompatibility = 1.6
1717
}
@@ -21,8 +21,10 @@ dependencies {
2121
compile "org.apache.httpcomponents:httpclient-cache:4.3.6"
2222
compile "commons-codec:commons-codec:1.5"
2323
compile "com.google.code.gson:gson:2.2.4"
24+
compile "com.google.guava:guava:19.0"
2425
compile "org.slf4j:slf4j-api:1.7.7"
2526
compile "org.glassfish.jersey.media:jersey-media-sse:2.20"
27+
compile "redis.clients:jedis:2.8.0"
2628
testCompile "org.easymock:easymock:3.3"
2729
testCompile 'junit:junit:[4.10,)'
2830
testRuntime "org.slf4j:slf4j-simple:1.7.7"

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public final class LDConfig {
3333
final HttpHost proxyHost;
3434
final boolean stream;
3535
final boolean debugStreaming;
36+
final FeatureStore featureStore;
3637

3738
protected LDConfig(Builder builder) {
3839
this.baseURI = builder.baseURI;
@@ -44,6 +45,7 @@ protected LDConfig(Builder builder) {
4445
this.streamURI = builder.streamURI;
4546
this.stream = builder.stream;
4647
this.debugStreaming = builder.debugStreaming;
48+
this.featureStore = builder.featureStore;
4749
}
4850

4951
/**
@@ -70,6 +72,7 @@ public static class Builder{
7072
private String proxyScheme;
7173
private boolean stream = true;
7274
private boolean debugStreaming = false;
75+
private FeatureStore featureStore = new InMemoryFeatureStore();
7376

7477
/**
7578
* Creates a builder with all configuration parameters set to the default
@@ -97,6 +100,11 @@ public Builder streamURI(URI streamURI) {
97100
return this;
98101
}
99102

103+
public Builder featureStore(FeatureStore store) {
104+
this.featureStore = store;
105+
return this;
106+
}
107+
100108
/**
101109
* Set whether we should debug streaming mode. If set, the client will fetch features via polling and compare the
102110
* retrieved feature with the value in the feature store. There is a performance cost to this, so it is not
@@ -266,7 +274,6 @@ public LDConfig build() {
266274

267275
}
268276

269-
270277
private URIBuilder getBuilder() {
271278
return new URIBuilder()
272279
.setScheme(baseURI.getScheme())
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package com.launchdarkly.client;
2+
3+
import com.google.common.cache.CacheBuilder;
4+
import com.google.common.cache.CacheLoader;
5+
import com.google.common.cache.LoadingCache;
6+
import com.google.gson.Gson;
7+
import com.google.gson.reflect.TypeToken;
8+
import org.apache.http.util.EntityUtils;
9+
import redis.clients.jedis.Jedis;
10+
import redis.clients.jedis.Transaction;
11+
12+
import java.lang.reflect.Type;
13+
import java.net.URI;
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
import java.util.concurrent.TimeUnit;
17+
18+
public class RedisFeatureStore implements FeatureStore {
19+
private static final String DEFAULT_PREFIX = "launchdarkly";
20+
private final Jedis jedis;
21+
private LoadingCache<String, FeatureRep<?>> cache;
22+
private String prefix;
23+
24+
public RedisFeatureStore(String host, int port, String prefix, long cacheTimeSecs) {
25+
jedis = new Jedis(host, port);
26+
setPrefix(prefix);
27+
createCache(cacheTimeSecs);
28+
}
29+
30+
public RedisFeatureStore(URI uri, String prefix, long cacheTimeSecs) {
31+
jedis = new Jedis(uri);
32+
setPrefix(prefix);
33+
createCache(cacheTimeSecs);
34+
}
35+
36+
public RedisFeatureStore() {
37+
jedis = new Jedis("localhost");
38+
this.prefix = DEFAULT_PREFIX;
39+
}
40+
41+
42+
private void setPrefix(String prefix) {
43+
if (prefix == null || prefix.isEmpty()) {
44+
this.prefix = DEFAULT_PREFIX;
45+
} else {
46+
this.prefix = prefix;
47+
}
48+
}
49+
50+
private void createCache(long cacheTimeSecs) {
51+
if (cacheTimeSecs > 0) {
52+
cache = CacheBuilder.newBuilder().expireAfterWrite(cacheTimeSecs, TimeUnit.SECONDS).build(new CacheLoader<String, FeatureRep<?>>() {
53+
54+
@Override
55+
public FeatureRep<?> load(String key) throws Exception {
56+
return getRedis(key);
57+
}
58+
});
59+
}
60+
}
61+
62+
63+
@Override
64+
public FeatureRep<?> get(String key) {
65+
if (cache != null) {
66+
return cache.getUnchecked(key);
67+
} else {
68+
return getRedis(key);
69+
}
70+
}
71+
72+
@Override
73+
public Map<String, FeatureRep<?>> all() {
74+
Map<String,String> featuresJson = jedis.hgetAll(featuresKey());
75+
Map<String, FeatureRep<?>> result = new HashMap<String, FeatureRep<?>>();
76+
Gson gson = new Gson();
77+
78+
Type type = new TypeToken<FeatureRep<?>>() {}.getType();
79+
80+
for (Map.Entry<String, String> entry : featuresJson.entrySet()) {
81+
FeatureRep<?> rep = gson.fromJson(entry.getValue(), type);
82+
result.put(entry.getKey(), rep);
83+
}
84+
85+
return result;
86+
}
87+
88+
@Override
89+
public void init(Map<String, FeatureRep<?>> features) {
90+
Gson gson = new Gson();
91+
Transaction t = jedis.multi();
92+
93+
t.del(featuresKey());
94+
95+
for (FeatureRep<?> f: features.values()) {
96+
t.hset(featuresKey(), f.key, gson.toJson(f));
97+
}
98+
99+
t.exec();
100+
}
101+
102+
@Override
103+
public void delete(String key, int version) {
104+
try {
105+
Gson gson = new Gson();
106+
jedis.watch(featuresKey());
107+
108+
FeatureRep<?> feature = getRedis(key);
109+
110+
if (feature != null && feature.version >= version) {
111+
return;
112+
}
113+
114+
feature.deleted = true;
115+
feature.version = version;
116+
117+
jedis.hset(featuresKey(), key, gson.toJson(feature));
118+
119+
if (cache != null) {
120+
cache.invalidate(key);
121+
}
122+
}
123+
finally {
124+
jedis.unwatch();
125+
}
126+
127+
}
128+
129+
@Override
130+
public void upsert(String key, FeatureRep<?> feature) {
131+
try {
132+
Gson gson = new Gson();
133+
jedis.watch(featuresKey());
134+
135+
FeatureRep<?> f = getRedis(key);
136+
137+
if (f != null && f.version >= feature.version) {
138+
return;
139+
}
140+
141+
jedis.hset(featuresKey(), key, gson.toJson(feature));
142+
143+
if (cache != null) {
144+
cache.invalidate(key);
145+
}
146+
}
147+
finally {
148+
jedis.unwatch();
149+
}
150+
}
151+
152+
@Override
153+
public boolean initialized() {
154+
return jedis.exists(featuresKey());
155+
}
156+
157+
158+
private String featuresKey() {
159+
return prefix + ":features";
160+
}
161+
162+
private FeatureRep<?> getRedis(String key) {
163+
Gson gson = new Gson();
164+
String featureJson = jedis.hget(featuresKey(), key);
165+
166+
if (featureJson == null) {
167+
return null;
168+
}
169+
170+
Type type = new TypeToken<FeatureRep<?>>() {}.getType();
171+
FeatureRep<?> f = gson.fromJson(featureJson, type);
172+
173+
return f.deleted ? null : f;
174+
}
175+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class StreamProcessor implements Closeable {
3535

3636
StreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) {
3737
this.client = ClientBuilder.newBuilder().register(SseFeature.class).build();
38-
this.store = new InMemoryFeatureStore();
38+
this.store = config.featureStore;
3939
this.config = config;
4040
this.apiKey = apiKey;
4141
this.requestor = requestor;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Variation<E> {
1515
int weight;
1616
TargetRule userTarget;
1717
List<TargetRule> targets;
18-
private final Logger logger = LoggerFactory.getLogger(Variation.class);
18+
private final static Logger logger = LoggerFactory.getLogger(Variation.class);
1919

2020
public Variation() {
2121

@@ -117,7 +117,7 @@ static class TargetRule {
117117
String operator;
118118
List<JsonPrimitive> values;
119119

120-
private final Logger logger = LoggerFactory.getLogger(TargetRule.class);
120+
private final static Logger logger = LoggerFactory.getLogger(TargetRule.class);
121121

122122
public TargetRule() {
123123

src/test/java/com/launchdarkly/client/FeatureRepTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public void testFlagForTargetedUserOn() {
102102
@Test
103103
public void testFlagForTargetGroupOn() {
104104
LDUser user = new LDUser.Builder("[email protected]")
105-
.custom("groups", Arrays.asList("google", "microsoft"))
105+
.customString("groups", Arrays.asList("google", "microsoft"))
106106
.build();
107107

108108
Boolean b = simpleFlag.evaluate(user);

0 commit comments

Comments
 (0)