99import com .launchdarkly .sdk .server .subsystems .DataSource ;
1010import com .launchdarkly .sdk .server .subsystems .DataSourceUpdateSinkV2 ;
1111
12- import java .io .IOException ;
1312import java .util .ArrayList ;
1413import java .util .Collections ;
1514import java .util .Date ;
@@ -48,8 +47,6 @@ class FDv2DataSource implements DataSource {
4847
4948 private final LDLogger logger ;
5049
51- private final DataSourceFactory <Synchronizer > fdv1DataSourceFactory ;
52-
5350 public interface DataSourceFactory <T > {
5451 T build ();
5552 }
@@ -92,7 +89,19 @@ public FDv2DataSource(
9289 .stream ()
9390 .map (SynchronizerFactoryWithState ::new )
9491 .collect (Collectors .toList ());
95- this .fdv1DataSourceFactory = fdv1DataSourceFactory ;
92+
93+ // If we have a fdv1 data source factory, then add that to the synchronizer factories in a blocked state.
94+ // If we receive a request to fallback, then we will unblock it and block all other synchronizers.
95+ if (fdv1DataSourceFactory != null ) {
96+ SynchronizerFactoryWithState wrapped = new SynchronizerFactoryWithState (fdv1DataSourceFactory ,
97+ true );
98+ wrapped .block ();
99+ synchronizerFactories .add (wrapped );
100+
101+ // Currently, we only support 1 fdv1 fallback synchronizer, but that limitation is introduced by the
102+ // configuration.
103+ }
104+
96105 this .synchronizerStateManager = new SynchronizerStateManager (synchronizerFactories );
97106 this .dataSourceUpdates = dataSourceUpdates ;
98107 this .threadPriority = threadPriority ;
@@ -104,6 +113,11 @@ public FDv2DataSource(
104113
105114 private void run () {
106115 Thread runThread = new Thread (() -> {
116+ if (initializers .isEmpty () && synchronizerStateManager .getAvailableSynchronizerCount () == 0 ) {
117+ dataSourceUpdates .updateStatus (DataSourceStatusProvider .State .VALID , null );
118+ startFuture .complete (true );
119+ return ;
120+ }
107121 if (!initializers .isEmpty ()) {
108122 runInitializers ();
109123 }
@@ -113,7 +127,7 @@ private void run() {
113127 new DataSourceStatusProvider .ErrorInfo (
114128 DataSourceStatusProvider .ErrorKind .UNKNOWN ,
115129 0 ,
116- "" ,
130+ "All data source acquisition methods have been exhausted. " ,
117131 new Date ().toInstant ())
118132 );
119133
@@ -146,12 +160,37 @@ private void runInitializers() {
146160 }
147161 break ;
148162 case STATUS :
149- // TODO: Implement.
163+ FDv2SourceResult .Status status = result .getStatus ();
164+ switch (status .getState ()) {
165+ case INTERRUPTED :
166+ case TERMINAL_ERROR :
167+ // The data source updates handler will filter the state during initializing, but this
168+ // will make the error information available.
169+ dataSourceUpdates .updateStatus (
170+ // While the error was terminal to the individual initializer, it isn't terminal
171+ // to the data source as a whole.
172+ DataSourceStatusProvider .State .INTERRUPTED ,
173+ status .getErrorInfo ());
174+ break ;
175+ case SHUTDOWN :
176+ case GOODBYE :
177+ // We don't need to inform anyone of these statuses.
178+ logger .debug ("Ignoring status {} from initializer" , result .getStatus ().getState ());
179+ break ;
180+ }
150181 break ;
151182 }
152183 } catch (ExecutionException | InterruptedException | CancellationException e ) {
153- // TODO: Better messaging?
154- // TODO: Data source status?
184+ // We don't expect these conditions to happen in practice.
185+
186+ // The data source updates handler will filter the state during initializing, but this
187+ // will make the error information available.
188+ dataSourceUpdates .updateStatus (
189+ DataSourceStatusProvider .State .INTERRUPTED ,
190+ new DataSourceStatusProvider .ErrorInfo (DataSourceStatusProvider .ErrorKind .UNKNOWN ,
191+ 0 ,
192+ e .toString (),
193+ new Date ().toInstant ()));
155194 logger .warn ("Error running initializer: {}" , e .toString ());
156195 }
157196 }
@@ -161,6 +200,8 @@ private void runInitializers() {
161200 dataSourceUpdates .updateStatus (DataSourceStatusProvider .State .VALID , null );
162201 startFuture .complete (true );
163202 }
203+ // If no data was received, then it is possible initialization will complete from synchronizers, so we give
204+ // them an opportunity to run before reporting any issues.
164205 }
165206
166207 /**
@@ -217,11 +258,13 @@ private void runSynchronizers() {
217258 // For fallback, we will move to the next available synchronizer, which may loop.
218259 // This is the default behavior of exiting the run loop, so we don't need to take
219260 // any action.
261+ logger .debug ("A synchronizer has experienced an interruption and we are falling back." );
220262 break ;
221263 case RECOVERY :
222264 // For recovery, we will start at the first available synchronizer.
223265 // So we reset the source index, and finding the source will start at the beginning.
224266 synchronizerStateManager .resetSourceIndex ();
267+ logger .debug ("The data source is attempting to recover to a higher priority synchronizer." );
225268 break ;
226269 }
227270 // A running synchronizer will only have fallback and recovery conditions that it can act on.
@@ -230,7 +273,7 @@ private void runSynchronizers() {
230273 break ;
231274 }
232275
233- if (!(res instanceof FDv2SourceResult )) {
276+ if (!(res instanceof FDv2SourceResult )) {
234277 logger .error ("Unexpected result type from synchronizer: {}" , res .getClass ().getName ());
235278 continue ;
236279 }
@@ -241,6 +284,7 @@ private void runSynchronizers() {
241284 switch (result .getResultType ()) {
242285 case CHANGE_SET :
243286 dataSourceUpdates .apply (result .getChangeSet ());
287+ dataSourceUpdates .updateStatus (DataSourceStatusProvider .State .VALID , null );
244288 // This could have been completed by any data source. But if it has not been completed before
245289 // now, then we complete it.
246290 startFuture .complete (true );
@@ -250,15 +294,20 @@ private void runSynchronizers() {
250294 switch (status .getState ()) {
251295 case INTERRUPTED :
252296 // Handled by conditions.
253- // TODO: Data source status.
297+ dataSourceUpdates .updateStatus (
298+ DataSourceStatusProvider .State .INTERRUPTED ,
299+ status .getErrorInfo ());
254300 break ;
255301 case SHUTDOWN :
256302 // We should be overall shutting down.
257- // TODO: We may need logging or to do a little more.
258- return false ;
303+ logger . debug ( "Synchronizer shutdown." );
304+ return ;
259305 case TERMINAL_ERROR :
260306 availableSynchronizer .block ();
261307 running = false ;
308+ dataSourceUpdates .updateStatus (
309+ DataSourceStatusProvider .State .INTERRUPTED ,
310+ status .getErrorInfo ());
262311 break ;
263312 case GOODBYE :
264313 // We let the synchronizer handle this internally.
@@ -268,14 +317,29 @@ private void runSynchronizers() {
268317 }
269318 // We have been requested to fall back to FDv1. We handle whatever message was associated,
270319 // close the synchronizer, and then fallback.
271- if (result .isFdv1Fallback ()) {
272- // return true;
320+ // Only trigger fallback if we're not already running the FDv1 fallback synchronizer.
321+ if (
322+ result .isFdv1Fallback () &&
323+ synchronizerStateManager .hasFDv1Fallback () &&
324+ // This shouldn't happen in practice, an FDv1 source shouldn't request fallback
325+ // to FDv1. But if it does, then we will discard its request.
326+ !availableSynchronizer .isFDv1Fallback ()
327+ ) {
328+ synchronizerStateManager .fdv1Fallback ();
329+ running = false ;
273330 }
274331 }
275332 }
276333 } catch (ExecutionException | InterruptedException | CancellationException e ) {
277- // TODO: Log.
278- // Move to next synchronizer.
334+ dataSourceUpdates .updateStatus (DataSourceStatusProvider .State .INTERRUPTED ,
335+ new DataSourceStatusProvider .ErrorInfo (
336+ DataSourceStatusProvider .ErrorKind .UNKNOWN ,
337+ 0 ,
338+ e .toString (),
339+ new Date ().toInstant ()
340+ ));
341+ logger .warn ("Error running synchronizer: {}, will try next synchronizer, or retry." , e .toString ());
342+ // Move to the next synchronizer.
279343 }
280344 availableSynchronizer = synchronizerStateManager .getNextAvailableSynchronizer ();
281345 }
0 commit comments