Skip to content

Commit 86aa10e

Browse files
committed
More graceful shutdown.
1 parent 5b3c669 commit 86aa10e

File tree

5 files changed

+207
-80
lines changed

5 files changed

+207
-80
lines changed

lib/sdk/server/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ private LDConfig buildSdkConfig(SdkConfigParams params, String tag) {
579579

580580
// Create and configure FDv1 fallback
581581
ComponentConfigurer<DataSource> fdv1Fallback =
582-
createFDv1FallbackSynchronizer(fallbackSynchronizer, endpoints);
582+
createFDv1FallbackSynchronizer(fallbackSynchronizer);
583583
dataSystemBuilder.fDv1FallbackSynchronizer(fdv1Fallback);
584584
}
585585

@@ -654,11 +654,10 @@ private static SdkConfigSynchronizerParams selectFallbackSynchronizer(
654654

655655
/**
656656
* Creates the FDv1 fallback synchronizer based on the selected synchronizer config.
657-
* FDv1 fallback is always polling-based.
657+
* FDv1 fallback is always polling-based and uses the global service endpoints configuration.
658658
*/
659659
private static ComponentConfigurer<DataSource> createFDv1FallbackSynchronizer(
660-
SdkConfigSynchronizerParams synchronizer,
661-
ServiceEndpointsBuilder endpoints) {
660+
SdkConfigSynchronizerParams synchronizer) {
662661

663662
// FDv1 fallback is always polling-based
664663
PollingDataSourceBuilder fdv1Polling = Components.pollingDataSource();
@@ -669,7 +668,8 @@ private static ComponentConfigurer<DataSource> createFDv1FallbackSynchronizer(
669668
fdv1Polling.pollInterval(Duration.ofMillis(synchronizer.polling.pollIntervalMs));
670669
}
671670
// Note: FDv1 polling doesn't support per-source service endpoints override,
672-
// so it will use the global service endpoints configuration
671+
// so it will use the global service endpoints configuration (which is set
672+
// by the caller before this method is invoked)
673673
}
674674
// If streaming synchronizer, use default polling interval
675675
// (no additional configuration needed)

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/FDv2DataSource.java

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ class FDv2DataSource implements DataSource {
3333
*/
3434
private static final long defaultRecoveryTimeout = 5 * 60;
3535

36-
private final List<DataSourceFactory<Initializer>> initializers;
37-
private final SynchronizerStateManager synchronizerStateManager;
36+
private final SourceStateManager sourceStateManager;
3837

3938
private final List<ConditionFactory> conditionFactories;
4039

@@ -47,6 +46,8 @@ class FDv2DataSource implements DataSource {
4746

4847
private final LDLogger logger;
4948

49+
private volatile boolean closed = false;
50+
5051
public interface DataSourceFactory<T> {
5152
T build();
5253
}
@@ -84,7 +85,6 @@ public FDv2DataSource(
8485
long fallbackTimeout,
8586
long recoveryTimeout
8687
) {
87-
this.initializers = initializers;
8888
List<SynchronizerFactoryWithState> synchronizerFactories = synchronizers
8989
.stream()
9090
.map(SynchronizerFactoryWithState::new)
@@ -102,7 +102,7 @@ public FDv2DataSource(
102102
// configuration.
103103
}
104104

105-
this.synchronizerStateManager = new SynchronizerStateManager(synchronizerFactories);
105+
this.sourceStateManager = new SourceStateManager(synchronizerFactories, initializers);
106106
this.dataSourceUpdates = dataSourceUpdates;
107107
this.threadPriority = threadPriority;
108108
this.logger = logger;
@@ -113,30 +113,33 @@ public FDv2DataSource(
113113

114114
private void run() {
115115
Thread runThread = new Thread(() -> {
116-
if (initializers.isEmpty() && synchronizerStateManager.getAvailableSynchronizerCount() == 0) {
116+
if (!sourceStateManager.hasAvailableSources()) {
117117
// There are not any initializer or synchronizers, so we are at the best state that
118118
// can be achieved.
119119
dataSourceUpdates.updateStatus(DataSourceStatusProvider.State.VALID, null);
120120
startFuture.complete(true);
121121
return;
122122
}
123-
if (!initializers.isEmpty()) {
123+
if (sourceStateManager.hasInitializers()) {
124124
runInitializers();
125125
}
126-
boolean synchronizersAvailable = synchronizerStateManager.getAvailableSynchronizerCount() != 0;
126+
boolean synchronizersAvailable = sourceStateManager.hasAvailableSynchronizers();
127127
if(!synchronizersAvailable) {
128128
// If already completed by the initializers, then this will have no effect.
129-
startFuture.complete(false);
130-
if (!isInitialized()) {
129+
if (!isInitialized() && !closed) {
130+
// If we were closed, then closing would have handled our terminal update.
131131
dataSourceUpdates.updateStatus(
132132
DataSourceStatusProvider.State.OFF,
133+
// If we were shutdown during initialization, then we don't need to include an error.
133134
new DataSourceStatusProvider.ErrorInfo(
134135
DataSourceStatusProvider.ErrorKind.UNKNOWN,
135136
0,
136137
"Initializers exhausted and there are no synchronizers",
137138
new Date().toInstant())
138139
);
139140
}
141+
// If already completed has no effect.
142+
startFuture.complete(false);
140143
return;
141144
}
142145

@@ -145,7 +148,9 @@ private void run() {
145148

146149
dataSourceUpdates.updateStatus(
147150
DataSourceStatusProvider.State.OFF,
148-
new DataSourceStatusProvider.ErrorInfo(
151+
// If the data source was closed, then we just report we are OFF without an
152+
// associated error.
153+
closed? null : new DataSourceStatusProvider.ErrorInfo(
149154
DataSourceStatusProvider.ErrorKind.UNKNOWN,
150155
0,
151156
"All data source acquisition methods have been exhausted.",
@@ -164,10 +169,11 @@ private void run() {
164169

165170
private void runInitializers() {
166171
boolean anyDataReceived = false;
167-
for (DataSourceFactory<Initializer> factory : initializers) {
172+
DataSourceFactory<Initializer> factory = sourceStateManager.getNextInitializer();
173+
while(factory != null) {
168174
try {
169175
Initializer initializer = factory.build();
170-
if (synchronizerStateManager.setActiveSource(initializer)) return;
176+
if (sourceStateManager.setActiveSource(initializer)) return;
171177
FDv2SourceResult result = initializer.run().get();
172178
switch (result.getResultType()) {
173179
case CHANGE_SET:
@@ -214,6 +220,7 @@ private void runInitializers() {
214220
new Date().toInstant()));
215221
logger.warn("Error running initializer: {}", e.toString());
216222
}
223+
factory = sourceStateManager.getNextInitializer();
217224
}
218225
// We received data without a selector, and we have exhausted initializers, so we are going to
219226
// consider ourselves initialized.
@@ -232,8 +239,8 @@ private void runInitializers() {
232239
* @return a list of conditions to apply to the synchronizer
233240
*/
234241
private List<Condition> getConditions() {
235-
int availableSynchronizers = synchronizerStateManager.getAvailableSynchronizerCount();
236-
boolean isPrimeSynchronizer = synchronizerStateManager.isPrimeSynchronizer();
242+
int availableSynchronizers = sourceStateManager.getAvailableSynchronizerCount();
243+
boolean isPrimeSynchronizer = sourceStateManager.isPrimeSynchronizer();
237244

238245
if (availableSynchronizers == 1) {
239246
// If there is only 1 synchronizer, then we cannot fall back or recover, so we don't need any conditions.
@@ -252,14 +259,14 @@ private List<Condition> getConditions() {
252259
private void runSynchronizers() {
253260
// When runSynchronizers exists, no matter how it exits, the synchronizerStateManager will be closed.
254261
try {
255-
SynchronizerFactoryWithState availableSynchronizer = synchronizerStateManager.getNextAvailableSynchronizer();
262+
SynchronizerFactoryWithState availableSynchronizer = sourceStateManager.getNextAvailableSynchronizer();
256263

257264
// We want to continue running synchronizers for as long as any are available.
258265
while (availableSynchronizer != null) {
259266
Synchronizer synchronizer = availableSynchronizer.build();
260267

261268
// Returns true if shutdown.
262-
if (synchronizerStateManager.setActiveSource(synchronizer)) return;
269+
if (sourceStateManager.setActiveSource(synchronizer)) return;
263270

264271
try {
265272
boolean running = true;
@@ -284,7 +291,7 @@ private void runSynchronizers() {
284291
case RECOVERY:
285292
// For recovery, we will start at the first available synchronizer.
286293
// So we reset the source index, and finding the source will start at the beginning.
287-
synchronizerStateManager.resetSourceIndex();
294+
sourceStateManager.resetSourceIndex();
288295
logger.debug("The data source is attempting to recover to a higher priority synchronizer.");
289296
break;
290297
}
@@ -341,12 +348,12 @@ private void runSynchronizers() {
341348
// Only trigger fallback if we're not already running the FDv1 fallback synchronizer.
342349
if (
343350
result.isFdv1Fallback() &&
344-
synchronizerStateManager.hasFDv1Fallback() &&
351+
sourceStateManager.hasFDv1Fallback() &&
345352
// This shouldn't happen in practice, an FDv1 source shouldn't request fallback
346353
// to FDv1. But if it does, then we will discard its request.
347354
!availableSynchronizer.isFDv1Fallback()
348355
) {
349-
synchronizerStateManager.fdv1Fallback();
356+
sourceStateManager.fdv1Fallback();
350357
running = false;
351358
}
352359
}
@@ -362,10 +369,12 @@ private void runSynchronizers() {
362369
logger.warn("Error running synchronizer: {}, will try next synchronizer, or retry.", e.toString());
363370
// Move to the next synchronizer.
364371
}
365-
availableSynchronizer = synchronizerStateManager.getNextAvailableSynchronizer();
372+
availableSynchronizer = sourceStateManager.getNextAvailableSynchronizer();
366373
}
367-
} finally {
368-
synchronizerStateManager.close();
374+
} catch(Exception e) {
375+
logger.error("Unexpected error in DataSource: {}", e.toString());
376+
}finally {
377+
sourceStateManager.close();
369378
}
370379
}
371380

@@ -388,11 +397,14 @@ public boolean isInitialized() {
388397

389398
@Override
390399
public void close() {
400+
closed = true;
391401
// If there is an active source, we will shut it down, and that will result in the loop handling that source
392402
// exiting.
393403
// If we do not have an active source, then the loop will check isShutdown when attempting to set one. When
394404
// it detects shutdown, it will exit the loop.
395-
synchronizerStateManager.close();
405+
sourceStateManager.close();
406+
407+
dataSourceUpdates.updateStatus(DataSourceStatusProvider.State.OFF, null);
396408

397409
// If this is already set, then this has no impact.
398410
startFuture.complete(false);

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/SynchronizerStateManager.java renamed to lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/SourceStateManager.java

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

3+
import com.launchdarkly.sdk.server.datasources.Initializer;
4+
35
import java.io.Closeable;
46
import java.io.IOException;
57
import java.util.List;
@@ -10,9 +12,11 @@
1012
* <p>
1113
* Package-private for internal use.
1214
*/
13-
class SynchronizerStateManager implements Closeable {
15+
class SourceStateManager implements Closeable {
1416
private final List<SynchronizerFactoryWithState> synchronizers;
1517

18+
private final List<FDv2DataSource.DataSourceFactory<Initializer>> initializers;
19+
1620
/**
1721
* Lock for active sources and shutdown state.
1822
*/
@@ -23,10 +27,13 @@ class SynchronizerStateManager implements Closeable {
2327
/**
2428
* We start at -1, so finding the next synchronizer can non-conditionally increment the index.
2529
*/
26-
private int sourceIndex = -1;
30+
private int synchronizerIndex = -1;
31+
32+
private int initializerIndex = -1;
2733

28-
public SynchronizerStateManager(List<SynchronizerFactoryWithState> synchronizers) {
34+
public SourceStateManager(List<SynchronizerFactoryWithState> synchronizers, List<FDv2DataSource.DataSourceFactory<Initializer>> initializers) {
2935
this.synchronizers = synchronizers;
36+
this.initializers = initializers;
3037
}
3138

3239
/**
@@ -35,7 +42,7 @@ public SynchronizerStateManager(List<SynchronizerFactoryWithState> synchronizers
3542
*/
3643
public void resetSourceIndex() {
3744
synchronized (activeSourceLock) {
38-
sourceIndex = -1;
45+
synchronizerIndex = -1;
3946
}
4047
}
4148

@@ -74,18 +81,24 @@ public SynchronizerFactoryWithState getNextAvailableSynchronizer() {
7481
synchronized (activeSourceLock) {
7582
SynchronizerFactoryWithState factory = null;
7683

84+
if(isShutdown) {
85+
safeClose(activeSource);
86+
activeSource = null;
87+
return factory;
88+
}
89+
7790
int visited = 0;
7891
while(visited < synchronizers.size()) {
7992
// Look for the next synchronizer starting at the position after the current one. (avoiding just re-using the same synchronizer.)
80-
sourceIndex++;
93+
synchronizerIndex++;
8194

8295
// We aren't using module here because we want to keep the stored index within range instead
8396
// of increasing indefinitely.
84-
if(sourceIndex >= synchronizers.size()) {
85-
sourceIndex = 0;
97+
if(synchronizerIndex >= synchronizers.size()) {
98+
synchronizerIndex = 0;
8699
}
87100

88-
SynchronizerFactoryWithState candidate = synchronizers.get(sourceIndex);
101+
SynchronizerFactoryWithState candidate = synchronizers.get(synchronizerIndex);
89102
if (candidate.getState() == SynchronizerFactoryWithState.State.Available) {
90103
factory = candidate;
91104
break;
@@ -96,6 +109,31 @@ public SynchronizerFactoryWithState getNextAvailableSynchronizer() {
96109
}
97110
}
98111

112+
public boolean hasAvailableSources() {
113+
return hasInitializers() || getAvailableSynchronizerCount() > 0;
114+
}
115+
116+
public boolean hasInitializers() {
117+
return !initializers.isEmpty();
118+
}
119+
120+
public boolean hasAvailableSynchronizers() {
121+
return getAvailableSynchronizerCount() > 0;
122+
}
123+
124+
public FDv2DataSource.DataSourceFactory<Initializer> getNextInitializer() {
125+
synchronized (activeSourceLock) {
126+
if(isShutdown) {
127+
return null;
128+
}
129+
initializerIndex++;
130+
if (initializerIndex >= initializers.size()) {
131+
return null;
132+
}
133+
return initializers.get(initializerIndex);
134+
}
135+
}
136+
99137
/**
100138
* Determine if the currently active synchronizer is the prime (first available) synchronizer.
101139
* @return true if the current synchronizer is the prime synchronizer, false otherwise
@@ -104,7 +142,7 @@ public boolean isPrimeSynchronizer() {
104142
synchronized (activeSourceLock) {
105143
for (int index = 0; index < synchronizers.size(); index++) {
106144
if (synchronizers.get(index).getState() == SynchronizerFactoryWithState.State.Available) {
107-
if (sourceIndex == index) {
145+
if (synchronizerIndex == index) {
108146
// This is the first synchronizer that is available, and it also is the current one.
109147
return true;
110148
}

0 commit comments

Comments
 (0)