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

Commit 1398c7c

Browse files
authored
Merge pull request #227 from launchdarkly/eb/ch67913/data-source-status
(#6) implement data source status monitoring
2 parents 2349290 + 9690842 commit 1398c7c

27 files changed

+1280
-241
lines changed

src/main/java/com/launchdarkly/sdk/server/Components.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.launchdarkly.sdk.server.interfaces.ClientContext;
1212
import com.launchdarkly.sdk.server.interfaces.DataSource;
1313
import com.launchdarkly.sdk.server.interfaces.DataSourceFactory;
14+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
1415
import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates;
1516
import com.launchdarkly.sdk.server.interfaces.DataStore;
1617
import com.launchdarkly.sdk.server.interfaces.DataStoreFactory;
@@ -338,6 +339,7 @@ public DataSource createDataSource(ClientContext context, DataSourceUpdates data
338339
} else {
339340
LDClient.logger.info("LaunchDarkly client will not connect to Launchdarkly for feature flag data");
340341
}
342+
dataSourceUpdates.updateStatus(DataSourceStatusProvider.State.VALID, null);
341343
return NullDataSource.INSTANCE;
342344
}
343345

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.launchdarkly.sdk.server;
2+
3+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
4+
5+
import java.util.function.Supplier;
6+
7+
final class DataSourceStatusProviderImpl implements DataSourceStatusProvider {
8+
private final EventBroadcasterImpl<DataSourceStatusProvider.StatusListener, DataSourceStatusProvider.Status> dataSourceStatusNotifier;
9+
private final Supplier<DataSourceStatusProvider.Status> statusSupplier;
10+
11+
DataSourceStatusProviderImpl(EventBroadcasterImpl<StatusListener, Status> dataSourceStatusNotifier,
12+
Supplier<Status> statusSupplier) {
13+
this.dataSourceStatusNotifier = dataSourceStatusNotifier;
14+
this.statusSupplier = statusSupplier;
15+
}
16+
17+
@Override
18+
public Status getStatus() {
19+
return statusSupplier.get();
20+
}
21+
22+
@Override
23+
public void addStatusListener(StatusListener listener) {
24+
dataSourceStatusNotifier.register(listener);
25+
}
26+
27+
@Override
28+
public void removeStatusListener(StatusListener listener) {
29+
dataSourceStatusNotifier.unregister(listener);
30+
}
31+
}

src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@
33
import com.google.common.collect.ImmutableMap;
44
import com.google.common.collect.ImmutableSet;
55
import com.launchdarkly.sdk.server.DataModelDependencies.KindAndKey;
6+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
7+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo;
8+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorKind;
9+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State;
10+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.Status;
11+
import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates;
612
import com.launchdarkly.sdk.server.interfaces.DataStore;
713
import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider;
814
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind;
915
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet;
1016
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor;
1117
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems;
12-
import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates;
1318
import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent;
1419
import com.launchdarkly.sdk.server.interfaces.FlagChangeListener;
1520

21+
import java.time.Instant;
1622
import java.util.HashMap;
1723
import java.util.HashSet;
1824
import java.util.Map;
@@ -33,34 +39,51 @@
3339
final class DataSourceUpdatesImpl implements DataSourceUpdates {
3440
private final DataStore store;
3541
private final EventBroadcasterImpl<FlagChangeListener, FlagChangeEvent> flagChangeEventNotifier;
42+
private final EventBroadcasterImpl<DataSourceStatusProvider.StatusListener, DataSourceStatusProvider.Status> dataSourceStatusNotifier;
3643
private final DataModelDependencies.DependencyTracker dependencyTracker = new DataModelDependencies.DependencyTracker();
3744
private final DataStoreStatusProvider dataStoreStatusProvider;
3845

46+
private volatile DataSourceStatusProvider.Status currentStatus;
47+
private volatile boolean lastStoreUpdateFailed = false;
48+
3949
DataSourceUpdatesImpl(
4050
DataStore store,
51+
DataStoreStatusProvider dataStoreStatusProvider,
4152
EventBroadcasterImpl<FlagChangeListener, FlagChangeEvent> flagChangeEventNotifier,
42-
DataStoreStatusProvider dataStoreStatusProvider
53+
EventBroadcasterImpl<DataSourceStatusProvider.StatusListener, DataSourceStatusProvider.Status> dataSourceStatusNotifier
4354
) {
4455
this.store = store;
4556
this.flagChangeEventNotifier = flagChangeEventNotifier;
57+
this.dataSourceStatusNotifier = dataSourceStatusNotifier;
4658
this.dataStoreStatusProvider = dataStoreStatusProvider;
59+
60+
currentStatus = new DataSourceStatusProvider.Status(
61+
DataSourceStatusProvider.State.INITIALIZING,
62+
Instant.now(),
63+
null
64+
);
4765
}
4866

4967
@Override
50-
public void init(FullDataSet<ItemDescriptor> allData) {
68+
public boolean init(FullDataSet<ItemDescriptor> allData) {
5169
Map<DataKind, Map<String, ItemDescriptor>> oldData = null;
52-
53-
if (hasFlagChangeEventListeners()) {
54-
// Query the existing data if any, so that after the update we can send events for whatever was changed
55-
oldData = new HashMap<>();
56-
for (DataKind kind: ALL_DATA_KINDS) {
57-
KeyedItems<ItemDescriptor> items = store.getAll(kind);
58-
oldData.put(kind, ImmutableMap.copyOf(items.getItems()));
70+
71+
try {
72+
if (hasFlagChangeEventListeners()) {
73+
// Query the existing data if any, so that after the update we can send events for whatever was changed
74+
oldData = new HashMap<>();
75+
for (DataKind kind: ALL_DATA_KINDS) {
76+
KeyedItems<ItemDescriptor> items = store.getAll(kind);
77+
oldData.put(kind, ImmutableMap.copyOf(items.getItems()));
78+
}
5979
}
80+
store.init(DataModelDependencies.sortAllCollections(allData));
81+
lastStoreUpdateFailed = false;
82+
} catch (RuntimeException e) {
83+
reportStoreFailure(e);
84+
return false;
6085
}
6186

62-
store.init(DataModelDependencies.sortAllCollections(allData));
63-
6487
// We must always update the dependency graph even if we don't currently have any event listeners, because if
6588
// listeners are added later, we don't want to have to reread the whole data store to compute the graph
6689
updateDependencyTrackerFromFullDataSet(allData);
@@ -70,11 +93,20 @@ public void init(FullDataSet<ItemDescriptor> allData) {
7093
if (oldData != null) {
7194
sendChangeEvents(computeChangedItemsForFullDataSet(oldData, fullDataSetToMap(allData)));
7295
}
96+
97+
return true;
7398
}
7499

75100
@Override
76-
public void upsert(DataKind kind, String key, ItemDescriptor item) {
77-
boolean successfullyUpdated = store.upsert(kind, key, item);
101+
public boolean upsert(DataKind kind, String key, ItemDescriptor item) {
102+
boolean successfullyUpdated;
103+
try {
104+
successfullyUpdated = store.upsert(kind, key, item);
105+
lastStoreUpdateFailed = false;
106+
} catch (RuntimeException e) {
107+
reportStoreFailure(e);
108+
return false;
109+
}
78110

79111
if (successfullyUpdated) {
80112
dependencyTracker.updateDependenciesFrom(kind, key, item);
@@ -84,21 +116,49 @@ public void upsert(DataKind kind, String key, ItemDescriptor item) {
84116
sendChangeEvents(affectedItems);
85117
}
86118
}
119+
120+
return true;
87121
}
88122

89123
@Override
90124
public DataStoreStatusProvider getDataStoreStatusProvider() {
91125
return dataStoreStatusProvider;
92126
}
93127

128+
@Override
129+
public void updateStatus(DataSourceStatusProvider.State newState, DataSourceStatusProvider.ErrorInfo newError) {
130+
if (newState == null) {
131+
return;
132+
}
133+
DataSourceStatusProvider.Status newStatus;
134+
synchronized (this) {
135+
if (newState == DataSourceStatusProvider.State.INTERRUPTED && currentStatus.getState() == DataSourceStatusProvider.State.INITIALIZING) {
136+
newState = DataSourceStatusProvider.State.INITIALIZING; // see comment on updateStatus in the DataSourceUpdates interface
137+
}
138+
if (newState == currentStatus.getState() && newError == null) {
139+
return;
140+
}
141+
currentStatus = new DataSourceStatusProvider.Status(
142+
newState,
143+
newState == currentStatus.getState() ? currentStatus.getStateSince() : Instant.now(),
144+
newError == null ? currentStatus.getLastError() : newError
145+
);
146+
newStatus = currentStatus;
147+
}
148+
dataSourceStatusNotifier.broadcast(newStatus);
149+
}
150+
151+
Status getLastStatus() {
152+
synchronized (this) {
153+
return currentStatus;
154+
}
155+
}
156+
94157
private boolean hasFlagChangeEventListeners() {
95-
return flagChangeEventNotifier != null && flagChangeEventNotifier.hasListeners();
158+
return flagChangeEventNotifier.hasListeners();
96159
}
97160

98161
private void sendChangeEvents(Iterable<KindAndKey> affectedItems) {
99-
if (flagChangeEventNotifier == null) {
100-
return;
101-
}
102162
for (KindAndKey item: affectedItems) {
103163
if (item.kind == FEATURES) {
104164
flagChangeEventNotifier.broadcast(new FlagChangeEvent(item.key));
@@ -153,6 +213,15 @@ private Set<KindAndKey> computeChangedItemsForFullDataSet(Map<DataKind, Map<Stri
153213
// version numbers are different, the higher one is the more recent version).
154214
}
155215
}
156-
return affectedItems;
216+
return affectedItems;
217+
}
218+
219+
private void reportStoreFailure(RuntimeException e) {
220+
if (!lastStoreUpdateFailed) {
221+
LDClient.logger.warn("Unexpected data store error when trying to store an update received from the data source: {}", e.toString());
222+
lastStoreUpdateFailed = true;
223+
}
224+
LDClient.logger.debug(e.toString(), e);
225+
updateStatus(State.INTERRUPTED, ErrorInfo.fromException(ErrorKind.STORE_ERROR, e));
157226
}
158227
}

src/main/java/com/launchdarkly/sdk/server/EventBroadcasterImpl.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package com.launchdarkly.sdk.server;
22

3+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
4+
import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider;
5+
import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent;
6+
import com.launchdarkly.sdk.server.interfaces.FlagChangeListener;
7+
38
import java.util.concurrent.CopyOnWriteArrayList;
49
import java.util.concurrent.ExecutorService;
510
import java.util.function.BiConsumer;
@@ -30,6 +35,20 @@ final class EventBroadcasterImpl<ListenerT, EventT> {
3035
this.executor = executor;
3136
}
3237

38+
static EventBroadcasterImpl<FlagChangeListener, FlagChangeEvent> forFlagChangeEvents(ExecutorService executor) {
39+
return new EventBroadcasterImpl<>(FlagChangeListener::onFlagChange, executor);
40+
}
41+
42+
static EventBroadcasterImpl<DataSourceStatusProvider.StatusListener, DataSourceStatusProvider.Status>
43+
forDataSourceStatus(ExecutorService executor) {
44+
return new EventBroadcasterImpl<>(DataSourceStatusProvider.StatusListener::dataSourceStatusChanged, executor);
45+
}
46+
47+
static EventBroadcasterImpl<DataStoreStatusProvider.StatusListener, DataStoreStatusProvider.Status>
48+
forDataStoreStatus(ExecutorService executor) {
49+
return new EventBroadcasterImpl<>(DataStoreStatusProvider.StatusListener::dataStoreStatusChanged, executor);
50+
}
51+
3352
/**
3453
* Registers a listener for this type of event. This method is thread-safe.
3554
*

src/main/java/com/launchdarkly/sdk/server/LDClient.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder;
99
import com.launchdarkly.sdk.server.interfaces.DataSource;
1010
import com.launchdarkly.sdk.server.interfaces.DataSourceFactory;
11-
import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates;
11+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
1212
import com.launchdarkly.sdk.server.interfaces.DataStore;
1313
import com.launchdarkly.sdk.server.interfaces.DataStoreFactory;
1414
import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider;
@@ -65,6 +65,7 @@ public final class LDClient implements LDClientInterface {
6565
final DataSource dataSource;
6666
final DataStore dataStore;
6767
private final DataStoreStatusProviderImpl dataStoreStatusProvider;
68+
private final DataSourceStatusProviderImpl dataSourceStatusProvider;
6869
private final EventBroadcasterImpl<FlagChangeListener, FlagChangeEvent> flagChangeEventNotifier;
6970
private final ScheduledExecutorService sharedExecutor;
7071

@@ -159,7 +160,7 @@ public LDClient(String sdkKey, LDConfig config) {
159160
DataStoreFactory factory = config.dataStoreFactory == null ?
160161
Components.inMemoryDataStore() : config.dataStoreFactory;
161162
EventBroadcasterImpl<DataStoreStatusProvider.StatusListener, DataStoreStatusProvider.Status> dataStoreStatusNotifier =
162-
new EventBroadcasterImpl<>(DataStoreStatusProvider.StatusListener::dataStoreStatusChanged, sharedExecutor);
163+
EventBroadcasterImpl.forDataStoreStatus(sharedExecutor);
163164
DataStoreUpdatesImpl dataStoreUpdates = new DataStoreUpdatesImpl(dataStoreStatusNotifier);
164165
this.dataStore = factory.createDataStore(context, dataStoreUpdates);
165166

@@ -173,19 +174,23 @@ public DataModel.Segment getSegment(String key) {
173174
}
174175
});
175176

176-
this.flagChangeEventNotifier = new EventBroadcasterImpl<>(FlagChangeListener::onFlagChange, sharedExecutor);
177+
this.flagChangeEventNotifier = EventBroadcasterImpl.forFlagChangeEvents(sharedExecutor);
177178

178179
this.dataStoreStatusProvider = new DataStoreStatusProviderImpl(this.dataStore, dataStoreUpdates);
179180

180181
DataSourceFactory dataSourceFactory = config.dataSourceFactory == null ?
181182
Components.streamingDataSource() : config.dataSourceFactory;
182-
DataSourceUpdates dataSourceUpdates = new DataSourceUpdatesImpl(
183+
EventBroadcasterImpl<DataSourceStatusProvider.StatusListener, DataSourceStatusProvider.Status> dataSourceStatusNotifier =
184+
EventBroadcasterImpl.forDataSourceStatus(sharedExecutor);
185+
DataSourceUpdatesImpl dataSourceUpdates = new DataSourceUpdatesImpl(
183186
dataStore,
187+
dataStoreStatusProvider,
184188
flagChangeEventNotifier,
185-
dataStoreStatusProvider
189+
dataSourceStatusNotifier
186190
);
187-
this.dataSource = dataSourceFactory.createDataSource(context, dataSourceUpdates);
188-
191+
this.dataSource = dataSourceFactory.createDataSource(context, dataSourceUpdates);
192+
this.dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceStatusNotifier, dataSourceUpdates::getLastStatus);
193+
189194
Future<Void> startFuture = dataSource.start();
190195
if (!config.startWait.isZero() && !config.startWait.isNegative()) {
191196
if (!(dataSource instanceof Components.NullDataSource)) {
@@ -460,6 +465,11 @@ public void unregisterFlagChangeListener(FlagChangeListener listener) {
460465
public DataStoreStatusProvider getDataStoreStatusProvider() {
461466
return dataStoreStatusProvider;
462467
}
468+
469+
@Override
470+
public DataSourceStatusProvider getDataSourceStatusProvider() {
471+
return dataSourceStatusProvider;
472+
}
463473

464474
@Override
465475
public void close() throws IOException {

src/main/java/com/launchdarkly/sdk/server/LDClientInterface.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.launchdarkly.sdk.EvaluationDetail;
44
import com.launchdarkly.sdk.LDUser;
55
import com.launchdarkly.sdk.LDValue;
6+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
67
import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider;
78
import com.launchdarkly.sdk.server.interfaces.FlagChangeListener;
89

@@ -263,6 +264,19 @@ public interface LDClientInterface extends Closeable {
263264
* @since 5.0.0
264265
*/
265266
void unregisterFlagChangeListener(FlagChangeListener listener);
267+
268+
/**
269+
* Returns an interface for tracking the status of the data source.
270+
* <p>
271+
* The data source is the mechanism that the SDK uses to get feature flag configurations, such as a
272+
* streaming connection (the default) or poll requests. The {@link DataSourceStatusProvider} has methods
273+
* for checking whether the data source is (as far as the SDK knows) currently operational and tracking
274+
* changes in this status.
275+
*
276+
* @return a {@link DataSourceStatusProvider}
277+
* @since 5.0.0
278+
*/
279+
DataSourceStatusProvider getDataSourceStatusProvider();
266280

267281
/**
268282
* Returns an interface for tracking the status of a persistent data store.

src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreStatusManager.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@ public void run() {
8585
};
8686
synchronized (this) {
8787
if (pollerFuture == null) {
88-
pollerFuture = scheduler.scheduleAtFixedRate(pollerTask, POLL_INTERVAL_MS, POLL_INTERVAL_MS, TimeUnit.MILLISECONDS);
88+
pollerFuture = scheduler.scheduleAtFixedRate(
89+
pollerTask,
90+
POLL_INTERVAL_MS,
91+
POLL_INTERVAL_MS,
92+
TimeUnit.MILLISECONDS
93+
);
8994
}
9095
}
9196
}

0 commit comments

Comments
 (0)