Skip to content

Commit 552adc3

Browse files
authored
Feature/v1.3.0 (#13)
1 parent 42c9c79 commit 552adc3

19 files changed

+663
-73
lines changed

README.md

+19-2
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ JAVA Server Side SDK is based on Java SE 8 and is available on Maven Central. Yo
3131
<dependency>
3232
<groupId>co.featbit</groupId>
3333
<artifactId>featbit-java-sdk</artifactId>
34-
<version>1.2.0</version>
34+
<version>1.3.0</version>
3535
</dependency>
3636
</dependencies>
3737
```
3838

3939
- Install the SDK using Gradle
4040
```
41-
implementation 'co.featbit:featbit-java-sdk:1.2.0'
41+
implementation 'co.featbit:featbit-java-sdk:1.3.0'
4242
```
4343

4444
### Prerequisite
@@ -286,6 +286,23 @@ String value = states.getString("flag key", user, "Not Found");
286286
> If evaluation called before Java SDK client initialized, you set the wrong flag key/user for the evaluation or the related feature flag
287287
is not found SDK will return the default value you set. `EvalDetail` will explain the details of the latest evaluation including error raison.
288288

289+
### Flag Tracking
290+
You can register registers a listener to be notified of feature flag changes in general.
291+
292+
Note that a flag value change listener is bound to a specific user and flag key.
293+
294+
The flag value change listener will be notified whenever the SDK receives any change to any feature flag's configuration, or to a user segment that is referenced by a feature flag.
295+
To register a flag value change listener, use 'FlagTracker#addFlagValueChangeListener' method.
296+
297+
The flag value change listener just call the `onFlagValueChange` method **_only if_** the flag value changes.
298+
299+
```java
300+
client.getFlagTracker().addFlagValueChangeListener(flagKey, user, event -> {
301+
// do something
302+
});
303+
```
304+
305+
289306
### Offline Mode
290307
In some situations, you might want to stop making remote calls to FeatBit. Here is how:
291308

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>co.featbit</groupId>
88
<artifactId>featbit-java-sdk</artifactId>
9-
<version>1.2.0</version>
9+
<version>1.3.0</version>
1010

1111
<name>featbit/featbit-java-sdk</name>
1212

src/main/java/co/featbit/server/DataModel.java

+15
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
import com.google.common.collect.ImmutableMap;
77
import com.google.gson.annotations.Expose;
88
import com.google.gson.annotations.JsonAdapter;
9+
import com.google.gson.reflect.TypeToken;
10+
import org.apache.commons.lang3.StringUtils;
911
import org.jetbrains.annotations.NotNull;
1012

1113
import java.util.Collections;
1214
import java.util.Date;
1315
import java.util.List;
1416
import java.util.Map;
17+
import java.util.stream.Collectors;
1518

1619
public abstract class DataModel {
1720

@@ -346,6 +349,18 @@ public String getVariationType() {
346349
return variationType;
347350
}
348351

352+
Boolean containsSegment(String segmentId) {
353+
return getRules().stream()
354+
.flatMap(rule -> rule.getConditions().stream())
355+
.filter(cond -> StringUtils.isBlank(cond.getOp()))
356+
.flatMap(cond -> {
357+
List<String> segments = JsonHelper
358+
.deserialize(cond.getValue(), new TypeToken<List<String>>() {
359+
}.getType());
360+
return segments.stream();
361+
}).collect(Collectors.toList()).contains(segmentId);
362+
}
363+
349364
@Override
350365
public void afterDeserialization() {
351366
this.timestamp = updatedAt.getTime();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package co.featbit.server;
2+
3+
public interface EventBroadcaster<Listener, Event> {
4+
void addListener(Listener listener);
5+
6+
void removeListener(Listener listener);
7+
8+
boolean hasListeners();
9+
10+
void broadcast(Event event);
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package co.featbit.server;
2+
3+
import org.slf4j.Logger;
4+
5+
import java.util.concurrent.CopyOnWriteArrayList;
6+
import java.util.concurrent.ExecutorService;
7+
import java.util.function.BiConsumer;
8+
9+
class EventBroadcasterImpl<Listener, Event> implements EventBroadcaster<Listener, Event> {
10+
private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
11+
private final BiConsumer<Listener, Event> broadcaster;
12+
private final ExecutorService executor;
13+
private final Logger logger;
14+
15+
EventBroadcasterImpl(
16+
BiConsumer<Listener, Event> broadcaster,
17+
ExecutorService executor,
18+
Logger logger
19+
) {
20+
this.broadcaster = broadcaster;
21+
this.executor = executor;
22+
this.logger = logger;
23+
}
24+
25+
static EventBroadcasterImpl<FlagChange.FlagChangeListener, FlagChange.FlagChangeEvent> forFlagChangeEvents(
26+
ExecutorService executor, Logger logger) {
27+
return new EventBroadcasterImpl<>(FlagChange.FlagChangeListener::onFlagChange, executor, logger);
28+
}
29+
30+
static EventBroadcasterImpl<Status.StateListener, Status.State> forDataUpdateStates(
31+
ExecutorService executor, Logger logger) {
32+
return new EventBroadcasterImpl<>(Status.StateListener::onStateChange, executor, logger);
33+
}
34+
35+
@Override
36+
public void addListener(Listener listener) {
37+
listeners.add(listener);
38+
}
39+
40+
@Override
41+
public void removeListener(Listener listener) {
42+
listeners.remove(listener);
43+
}
44+
45+
@Override
46+
public boolean hasListeners() {
47+
return !listeners.isEmpty();
48+
}
49+
50+
@Override
51+
public void broadcast(Event event) {
52+
if (executor == null) {
53+
return;
54+
}
55+
for (Listener listener : listeners) {
56+
executor.submit(() -> {
57+
try {
58+
broadcaster.accept(listener, event);
59+
} catch (Exception e) {
60+
logger.error("Unexpected exception in event listener", e);
61+
}
62+
});
63+
}
64+
}
65+
66+
67+
}

src/main/java/co/featbit/server/FBClientImp.java

+18-5
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
import java.io.IOException;
1414
import java.time.Duration;
1515
import java.util.Map;
16-
import java.util.concurrent.Future;
17-
import java.util.concurrent.TimeUnit;
18-
import java.util.concurrent.TimeoutException;
16+
import java.util.concurrent.*;
1917
import java.util.function.Consumer;
2018

2119
import static co.featbit.server.Evaluator.*;
@@ -36,8 +34,12 @@ public final class FBClientImp implements FBClient {
3634
private final Status.DataUpdater dataUpdater;
3735
private final InsightProcessor insightProcessor;
3836

37+
private final ExecutorService sharedExecutorService;
38+
3939
private final Consumer<InsightTypes.Event> eventHandler;
4040

41+
private final FlagTracker flagTracker;
42+
4143
/**
4244
* Creates a new client to connect to feature flag center with a specified configuration.
4345
* <p>
@@ -109,13 +111,18 @@ public FBClientImp(String envSecret, FBConfig config) {
109111
return item == null ? null : (DataModel.Segment) item;
110112
};
111113
this.evaluator = new EvaluatorImp(flagGetter, segmentGetter);
114+
115+
this.sharedExecutorService = new ScheduledThreadPoolExecutor(1, Utils.createThreadFactory("featbit-shared-worker-%d", true));
116+
EventBroadcasterImpl<Status.StateListener, Status.State> dataUpdateStateNotifier = EventBroadcasterImpl.forDataUpdateStates(this.sharedExecutorService, logger);
117+
EventBroadcasterImpl<FlagChange.FlagChangeListener, FlagChange.FlagChangeEvent> flagChangeEventNotifier = EventBroadcasterImpl.forFlagChangeEvents(this.sharedExecutorService, logger);
118+
this.flagTracker = new FlagTrackerImpl(flagChangeEventNotifier, (key, user) -> variation(key, user, null));
112119
//data updator
113-
Status.DataUpdaterImpl dataUpdatorImpl = new Status.DataUpdaterImpl(this.storage);
120+
Status.DataUpdaterImpl dataUpdatorImpl = new Status.DataUpdaterImpl(this.storage, dataUpdateStateNotifier, flagChangeEventNotifier);
114121
this.dataUpdater = dataUpdatorImpl;
115122
//data processor
116123
this.dataSynchronizer = config.getDataSynchronizerFactory().createDataSynchronizer(context, dataUpdatorImpl);
117124
//data update status provider
118-
this.dataUpdateStatusProvider = new Status.DataUpdateStatusProviderImpl(dataUpdatorImpl);
125+
this.dataUpdateStatusProvider = new Status.DataUpdateStatusProviderImpl(dataUpdatorImpl, dataUpdateStateNotifier);
119126

120127
// data sync
121128
Duration startWait = config.getStartWaitTime();
@@ -287,6 +294,12 @@ public void close() throws IOException {
287294
this.storage.close();
288295
this.dataSynchronizer.close();
289296
this.insightProcessor.close();
297+
this.sharedExecutorService.shutdownNow();
298+
}
299+
300+
@Override
301+
public FlagTracker getFlagTracker() {
302+
return this.flagTracker;
290303
}
291304

292305
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package co.featbit.server;
2+
3+
import java.util.Objects;
4+
5+
public abstract class FlagChange {
6+
7+
public static class FlagChangeEvent {
8+
private final String key;
9+
10+
public FlagChangeEvent(String key) {
11+
this.key = key;
12+
}
13+
14+
public String getKey() {
15+
return key;
16+
}
17+
18+
@Override
19+
public boolean equals(Object o) {
20+
if (this == o) return true;
21+
if (o == null || getClass() != o.getClass()) return false;
22+
FlagChangeEvent that = (FlagChangeEvent) o;
23+
return Objects.equals(key, that.key);
24+
}
25+
26+
@Override
27+
public int hashCode() {
28+
return Objects.hash(key);
29+
}
30+
}
31+
32+
public interface FlagChangeListener {
33+
void onFlagChange(FlagChangeEvent event);
34+
}
35+
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package co.featbit.server;
2+
3+
import co.featbit.commons.model.FBUser;
4+
import co.featbit.server.exterior.FlagTracker;
5+
import co.featbit.server.exterior.FlagValueChangeEvent;
6+
import co.featbit.server.exterior.FlagValueChangeListener;
7+
8+
import java.util.concurrent.atomic.AtomicReference;
9+
import java.util.function.BiFunction;
10+
11+
class FlagTrackerImpl implements FlagTracker {
12+
13+
private final EventBroadcaster<FlagChange.FlagChangeListener, FlagChange.FlagChangeEvent> flagChangeEventNotifier;
14+
15+
private final BiFunction<String, FBUser, Object> evaluateFn;
16+
17+
FlagTrackerImpl(EventBroadcaster<FlagChange.FlagChangeListener, FlagChange.FlagChangeEvent> flagChangeEventNotifier,
18+
BiFunction<String, FBUser, Object> evaluateFn) {
19+
this.flagChangeEventNotifier = flagChangeEventNotifier;
20+
this.evaluateFn = evaluateFn;
21+
}
22+
23+
@Override
24+
public FlagChange.FlagChangeListener addFlagValueChangeListener(String flagKey, FBUser user, FlagValueChangeListener listener) {
25+
FlagChange.FlagChangeListener adapter = new FlagValueChangeAdapter(flagKey, user, listener);
26+
addFlagChangeListener(adapter);
27+
return adapter;
28+
}
29+
30+
@Override
31+
public void removeFlagChangeListener(FlagChange.FlagChangeListener listener) {
32+
flagChangeEventNotifier.removeListener(listener);
33+
}
34+
35+
@Override
36+
public void addFlagChangeListener(FlagChange.FlagChangeListener listener) {
37+
flagChangeEventNotifier.addListener(listener);
38+
}
39+
40+
private final class FlagValueChangeAdapter implements FlagChange.FlagChangeListener {
41+
private final String flagKey;
42+
private final FBUser user;
43+
private final FlagValueChangeListener listener;
44+
private final AtomicReference<Object> value;
45+
46+
FlagValueChangeAdapter(String flagKey, FBUser user, FlagValueChangeListener listener) {
47+
this.flagKey = flagKey;
48+
this.user = user;
49+
this.listener = listener;
50+
this.value = new AtomicReference<>(evaluateFn.apply(flagKey, user));
51+
}
52+
53+
@Override
54+
public void onFlagChange(FlagChange.FlagChangeEvent event) {
55+
if (event.getKey().equals(flagKey)) {
56+
Object newValue = evaluateFn.apply(flagKey, user);
57+
Object oldValue = value.getAndSet(newValue);
58+
if (newValue != null && !newValue.equals(oldValue)) {
59+
listener.onFlagValueChange(new FlagValueChangeEvent(flagKey, oldValue, newValue));
60+
}
61+
}
62+
}
63+
64+
}
65+
}

src/main/java/co/featbit/server/Loggers.java

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ abstract class Loggers {
1717
static final Logger EVENTS = LoggerFactory.getLogger(EVENTS_LOGGER_NAME);
1818
private static final String UTILS_LOGGER_NAME = BASE_LOGGER_NAME + ".Utils";
1919
static final Logger UTILS = LoggerFactory.getLogger(UTILS_LOGGER_NAME);
20+
private static final String TEST_LOGGER_NAME = BASE_LOGGER_NAME + ".Test";
21+
static final Logger TEST = LoggerFactory.getLogger(TEST_LOGGER_NAME);
2022

2123
Loggers() {
2224
super();

0 commit comments

Comments
 (0)