Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7953896
basic unit tests for flag evaluation
eli-darkly Feb 7, 2018
da077e7
prepare 2.6.1 release (#118)
eli-darkly Mar 1, 2018
372966d
Merge branch '2.6.x' of github.com:launchdarkly/java-client into 2.6.x
eli-darkly Mar 1, 2018
999ff41
Update Changelog for release of version 2.6.1
eli-darkly Mar 1, 2018
4cff700
version 2.6.1
eli-darkly Mar 1, 2018
2e1584b
Preparing for release of version 2.6.1
eli-darkly Mar 1, 2018
8a8f0c6
fix error in javadoc code sample
eli-darkly Mar 1, 2018
cb9d26b
Merge pull request #54 from launchdarkly/eb/javadoc-fix
eli-darkly Mar 7, 2018
3a1d855
Merge branch '2.6.x'
eli-darkly Mar 7, 2018
0c79ac2
rm test file
eli-darkly Mar 10, 2018
7251919
Merge branch 'master' into eb/flag-tests
eli-darkly Mar 10, 2018
be75e5f
misc cleanup
eli-darkly Mar 10, 2018
bafc9b2
fix Redis optimistic locking logic to retry updates as needed
eli-darkly Mar 15, 2018
17d9d0d
we should persist deleted items in the local cache too
eli-darkly Mar 16, 2018
556c03c
better unit tests for Redis concurrent modification
eli-darkly Mar 19, 2018
1612b1f
Merge pull request #55 from launchdarkly/eb/flag-tests
eli-darkly Mar 22, 2018
e0863c5
Merge branch 'master' into eb/ch13390/redis-watch
eli-darkly Mar 22, 2018
3063cd2
treat unsupported operators as a non-match, don't throw NPE
eli-darkly Mar 22, 2018
ae3696a
add another unit test
eli-darkly Mar 22, 2018
91b0af8
Merge pull request #59 from launchdarkly/eb/ch15275/bad-operator-npe
eli-darkly Mar 22, 2018
d5704eb
Merge pull request #57 from launchdarkly/eb/ch13390/redis-watch
eli-darkly Mar 22, 2018
b48f6fb
reduce "same or lower version" log level to debug
eli-darkly Mar 26, 2018
cda543e
Merge pull request #60 from launchdarkly/eb/ch13390/redis-watch-log-l…
eli-darkly Mar 26, 2018
3305f2c
version 3.0.3
eli-darkly Mar 26, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ All notable changes to the LaunchDarkly Java SDK will be documented in this file
_This release was broken and should not be used._


## [2.6.1] - 2018-03-01
### Fixed
- Improved performance when evaluating flags with custom attributes, by avoiding an unnecessary caught exception (thanks, [rbalamohan](https://github.com/launchdarkly/java-client/issues/113)).


## [2.6.0] - 2018-02-12
## Added
- Adds support for a future LaunchDarkly feature, coming soon: semantic version user attributes.
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version=3.0.2
version=3.0.3
ossrhUsername=
ossrhPassword=
8 changes: 5 additions & 3 deletions src/main/java/com/launchdarkly/client/Clause.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,11 @@ boolean matchesUser(FeatureStore store, LDUser user) {
}

private boolean matchAny(JsonPrimitive userValue) {
for (JsonPrimitive v : values) {
if (op.apply(userValue, v)) {
return true;
if (op != null) {
for (JsonPrimitive v : values) {
if (op.apply(userValue, v)) {
return true;
}
}
}
return false;
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/launchdarkly/client/FeatureFlagBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.gson.JsonElement;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class FeatureFlagBuilder {
Expand Down Expand Up @@ -84,6 +85,10 @@ FeatureFlagBuilder variations(List<JsonElement> variations) {
return this;
}

FeatureFlagBuilder variations(JsonElement... variations) {
return variations(Arrays.asList(variations));
}

FeatureFlagBuilder deleted(boolean deleted) {
this.deleted = deleted;
return this;
Expand Down
173 changes: 81 additions & 92 deletions src/main/java/com/launchdarkly/client/RedisFeatureStore.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
package com.launchdarkly.client;

import static com.launchdarkly.client.VersionedDataKind.FEATURES;

import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
Expand All @@ -24,6 +11,20 @@
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import static com.launchdarkly.client.VersionedDataKind.FEATURES;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
Expand All @@ -38,12 +39,15 @@ public class RedisFeatureStore implements FeatureStore {
private static final String DEFAULT_PREFIX = "launchdarkly";
private static final String INIT_KEY = "$initialized$";
private static final String CACHE_REFRESH_THREAD_POOL_NAME_FORMAT = "RedisFeatureStore-cache-refresher-pool-%d";
private static final Gson gson = new Gson();

private final JedisPool pool;
private LoadingCache<CacheKey, Optional<VersionedData>> cache;
private final LoadingCache<String, Boolean> initCache = createInitCache();
private String prefix;
private ListeningExecutorService executorService;

private UpdateListener updateListener;

private static class CacheKey {
final VersionedDataKind<?> kind;
final String key;
Expand Down Expand Up @@ -102,10 +106,6 @@ private void setPrefix(String prefix) {
}
}

private void createCache(long cacheTimeSecs) {
createCache(cacheTimeSecs, false, false);
}

private void createCache(long cacheTimeSecs, boolean refreshStaleValues, boolean asyncRefresh) {
if (cacheTimeSecs > 0) {
if (refreshStaleValues) {
Expand All @@ -120,7 +120,9 @@ private CacheLoader<CacheKey, Optional<VersionedData>> createDefaultCacheLoader(
return new CacheLoader<CacheKey, Optional<VersionedData>>() {
@Override
public Optional<VersionedData> load(CacheKey key) throws Exception {
return Optional.<VersionedData>fromNullable(getRedis(key.kind, key.key));
try (Jedis jedis = pool.getResource()) {
return Optional.<VersionedData>fromNullable(getRedisEvenIfDeleted(key.kind, key.key, jedis));
}
}
};
}
Expand Down Expand Up @@ -169,7 +171,13 @@ public <T extends VersionedData> T get(VersionedDataKind<T> kind, String key) {
if (cache != null) {
item = (T) cache.getUnchecked(new CacheKey(kind, key)).orNull();
} else {
item = getRedis(kind, key);
try (Jedis jedis = pool.getResource()) {
item = getRedisEvenIfDeleted(kind, key, jedis);
}
}
if (item != null && item.isDeleted()) {
logger.debug("[get] Key: {} has been deleted in \"{}\". Returning null", key, kind.getNamespace());
return null;
}
if (item != null) {
logger.debug("[get] Key: {} with version: {} found in \"{}\".", key, item.getVersion(), kind.getNamespace());
Expand All @@ -182,7 +190,6 @@ public <T extends VersionedData> Map<String, T> all(VersionedDataKind<T> kind) {
try (Jedis jedis = pool.getResource()) {
Map<String, String> allJson = jedis.hgetAll(itemsKey(kind));
Map<String, T> result = new HashMap<>();
Gson gson = new Gson();

for (Map.Entry<String, String> entry : allJson.entrySet()) {
T item = gson.fromJson(entry.getValue(), kind.getItemClass());
Expand All @@ -197,7 +204,6 @@ public <T extends VersionedData> Map<String, T> all(VersionedDataKind<T> kind) {
@Override
public void init(Map<VersionedDataKind<?>, Map<String, ? extends VersionedData>> allData) {
try (Jedis jedis = pool.getResource()) {
Gson gson = new Gson();
Transaction t = jedis.multi();

for (Map.Entry<VersionedDataKind<?>, Map<String, ? extends VersionedData>> entry: allData.entrySet()) {
Expand All @@ -216,63 +222,54 @@ public void init(Map<VersionedDataKind<?>, Map<String, ? extends VersionedData>>

@Override
public <T extends VersionedData> void delete(VersionedDataKind<T> kind, String key, int version) {
Jedis jedis = null;
try {
Gson gson = new Gson();
jedis = pool.getResource();
String baseKey = itemsKey(kind);
jedis.watch(baseKey);

VersionedData item = getRedis(kind, key, jedis);

if (item != null && item.getVersion() >= version) {
logger.warn("Attempted to delete key: {} version: {}" +
" with a version that is the same or older: {} in \"{}\"",
key, item.getVersion(), version, kind.getNamespace());
return;
}

VersionedData deletedItem = kind.makeDeletedItem(key, version);
jedis.hset(baseKey, key, gson.toJson(deletedItem));

if (cache != null) {
cache.invalidate(new CacheKey(kind, key));
}
} finally {
if (jedis != null) {
jedis.unwatch();
jedis.close();
}
}
T deletedItem = kind.makeDeletedItem(key, version);
updateItemWithVersioning(kind, deletedItem);
}

@Override
public <T extends VersionedData> void upsert(VersionedDataKind<T> kind, T item) {
Jedis jedis = null;
try {
jedis = pool.getResource();
Gson gson = new Gson();
String baseKey = itemsKey(kind);
jedis.watch(baseKey);

VersionedData old = getRedisEvenIfDeleted(kind, item.getKey(), jedis);

if (old != null && old.getVersion() >= item.getVersion()) {
logger.warn("Attempted to update key: {} version: {}" +
" with a version that is the same or older: {} in \"{}\"",
item.getKey(), old.getVersion(), item.getVersion(), kind.getNamespace());
return;
}

jedis.hset(baseKey, item.getKey(), gson.toJson(item));
updateItemWithVersioning(kind, item);
}

if (cache != null) {
cache.invalidate(new CacheKey(kind, item.getKey()));
}
} finally {
if (jedis != null) {
jedis.unwatch();
jedis.close();
private <T extends VersionedData> void updateItemWithVersioning(VersionedDataKind<T> kind, T newItem) {
while (true) {
Jedis jedis = null;
try {
jedis = pool.getResource();
String baseKey = itemsKey(kind);
jedis.watch(baseKey);

if (updateListener != null) {
updateListener.aboutToUpdate(baseKey, newItem.getKey());
}

VersionedData oldItem = getRedisEvenIfDeleted(kind, newItem.getKey(), jedis);

if (oldItem != null && oldItem.getVersion() >= newItem.getVersion()) {
logger.debug("Attempted to {} key: {} version: {}" +
" with a version that is the same or older: {} in \"{}\"",
newItem.isDeleted() ? "delete" : "update",
newItem.getKey(), oldItem.getVersion(), newItem.getVersion(), kind.getNamespace());
return;
}

Transaction tx = jedis.multi();
tx.hset(baseKey, newItem.getKey(), gson.toJson(newItem));
List<Object> result = tx.exec();
if (result.isEmpty()) {
// if exec failed, it means the watch was triggered and we should retry
logger.debug("Concurrent modification detected, retrying");
continue;
}

if (cache != null) {
cache.invalidate(new CacheKey(kind, newItem.getKey()));
}
} finally {
if (jedis != null) {
jedis.unwatch();
jedis.close();
}
}
}
}
Expand Down Expand Up @@ -323,23 +320,7 @@ private Boolean getInit() {
}
}

private <T extends VersionedData> T getRedis(VersionedDataKind<T> kind, String key) {
try (Jedis jedis = pool.getResource()) {
return getRedis(kind, key, jedis);
}
}

private <T extends VersionedData> T getRedis(VersionedDataKind<T> kind, String key, Jedis jedis) {
T item = getRedisEvenIfDeleted(kind, key, jedis);
if (item != null && item.isDeleted()) {
logger.debug("[get] Key: {} has been deleted in \"{}\". Returning null", key, kind.getNamespace());
return null;
}
return item;
}

private <T extends VersionedData> T getRedisEvenIfDeleted(VersionedDataKind<T> kind, String key, Jedis jedis) {
Gson gson = new Gson();
String json = jedis.hget(itemsKey(kind), key);

if (json == null) {
Expand All @@ -354,4 +335,12 @@ private static JedisPoolConfig getPoolConfig() {
return new JedisPoolConfig();
}

static interface UpdateListener {
void aboutToUpdate(String baseKey, String itemKey);
}

@VisibleForTesting
void setUpdateListener(UpdateListener updateListener) {
this.updateListener = updateListener;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* {@link RedisFeatureStoreBuilder} calls can be chained, enabling the following pattern:
*
* <pre>
* RedisFeatureStoreBuilder builder = new RedisFeatureStoreBuilder("host", 443, 60)
* RedisFeatureStore store = new RedisFeatureStoreBuilder("host", 443, 60)
* .refreshStaleValues(true)
* .asyncRefresh(true)
* .socketTimeout(200)
Expand Down
Loading