Skip to content

Commit 0b070e6

Browse files
committed
[SDK-1956] Replace internal switchMode() with teardown/rebuild at ConnectivityManager level
Per updated CSFDV2 spec and JS implementation, mode switching now tears down the old data source and builds a new one rather than swapping internal synchronizers. Delete ModeAware interface, remove switchMode() from FDv2DataSource and switchSynchronizers() from SourceManager. FDv2DataSourceBuilder becomes the sole owner of mode resolution via setActiveMode()/build(), with ConnectivityManager using a useFDv2ModeResolution flag to route FDv2 through the new path while preserving FDv1 behavior. Implements CSFDV2 5.3.8 (retain data source when old and new modes share the same ModeDefinition). Made-with: Cursor
1 parent 24ed450 commit 0b070e6

9 files changed

Lines changed: 277 additions & 421 deletions

File tree

launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class ConnectivityManager {
7272
private final AtomicReference<Boolean> previouslyInBackground = new AtomicReference<>();
7373
private final LDLogger logger;
7474
private volatile boolean initialized = false;
75-
private volatile Map<ConnectionMode, ResolvedModeDefinition> resolvedModeTable;
75+
private final boolean useFDv2ModeResolution;
7676
private volatile ConnectionMode currentFDv2Mode;
7777

7878
// The DataSourceUpdateSinkImpl receives flag updates and status updates from the DataSource.
@@ -150,6 +150,7 @@ public void shutDown() {
150150
connectionInformation = new ConnectionInformationState();
151151
readStoredConnectionState();
152152
this.backgroundUpdatingDisabled = ldConfig.isDisableBackgroundPolling();
153+
this.useFDv2ModeResolution = (dataSourceFactory instanceof FDv2DataSourceBuilder);
153154

154155
connectivityChangeListener = networkAvailable -> handleModeStateChange();
155156
platformState.addConnectivityChangeListener(connectivityChangeListener);
@@ -190,13 +191,27 @@ private synchronized boolean updateDataSource(
190191
}
191192

192193
DataSource existingDataSource = currentDataSource.get();
194+
boolean isFDv2ModeSwitch = false;
193195

194-
// FDv2 ModeAware data sources handle all state transitions (including
195-
// offline/background) via mode resolution rather than teardown/rebuild.
196-
if (!mustReinitializeDataSource && existingDataSource instanceof ModeAware) {
197-
resolveAndSwitchMode((ModeAware) existingDataSource);
198-
onCompletion.onSuccess(null);
199-
return false;
196+
// FDv2 path: resolve mode and determine if a teardown/rebuild is needed.
197+
if (useFDv2ModeResolution && !mustReinitializeDataSource) {
198+
ConnectionMode newMode = resolveMode();
199+
if (newMode == currentFDv2Mode) {
200+
onCompletion.onSuccess(null);
201+
return false;
202+
}
203+
// CSFDV2 5.3.8: retain active data source if old and new modes have equivalent config.
204+
FDv2DataSourceBuilder fdv2Builder = (FDv2DataSourceBuilder) dataSourceFactory;
205+
ModeDefinition oldDef = fdv2Builder.getModeDefinition(currentFDv2Mode);
206+
ModeDefinition newDef = fdv2Builder.getModeDefinition(newMode);
207+
if (oldDef != null && oldDef == newDef) {
208+
currentFDv2Mode = newMode;
209+
onCompletion.onSuccess(null);
210+
return false;
211+
}
212+
currentFDv2Mode = newMode;
213+
isFDv2ModeSwitch = true;
214+
mustReinitializeDataSource = true;
200215
}
201216

202217
// FDv1 path: check whether the data source needs a full rebuild.
@@ -215,7 +230,12 @@ private synchronized boolean updateDataSource(
215230
boolean shouldStopExistingDataSource = true,
216231
shouldStartDataSourceIfStopped = false;
217232

218-
if (forceOffline) {
233+
if (useFDv2ModeResolution) {
234+
// FDv2 mode resolution already accounts for offline/background states via
235+
// the ModeResolutionTable, so we always rebuild when the mode changed.
236+
shouldStopExistingDataSource = mustReinitializeDataSource;
237+
shouldStartDataSourceIfStopped = true;
238+
} else if (forceOffline) {
219239
logger.debug("Initialized in offline mode");
220240
initialized = true;
221241
dataSourceUpdateSink.setStatus(ConnectionInformation.ConnectionMode.SET_OFFLINE, null);
@@ -249,41 +269,32 @@ private synchronized boolean updateDataSource(
249269
previouslyInBackground.get(),
250270
transactionalDataStore
251271
);
252-
DataSource dataSource = dataSourceFactory.build(clientContext);
253-
currentDataSource.set(dataSource);
254-
previouslyInBackground.set(Boolean.valueOf(inBackground));
255272

256-
if (dataSourceFactory instanceof FDv2DataSourceBuilder) {
273+
if (useFDv2ModeResolution) {
257274
FDv2DataSourceBuilder fdv2Builder = (FDv2DataSourceBuilder) dataSourceFactory;
258-
resolvedModeTable = fdv2Builder.getResolvedModeTable();
259-
currentFDv2Mode = fdv2Builder.getStartingMode();
275+
// CONNMODE 2.0.1: mode switches only transition synchronizers, not initializers.
276+
fdv2Builder.setActiveMode(currentFDv2Mode, !isFDv2ModeSwitch);
260277
}
261278

279+
DataSource dataSource = dataSourceFactory.build(clientContext);
280+
currentDataSource.set(dataSource);
281+
previouslyInBackground.set(Boolean.valueOf(inBackground));
282+
262283
dataSource.start(new Callback<Boolean>() {
263284
@Override
264285
public void onSuccess(Boolean result) {
265286
initialized = true;
266-
// passing the current connection mode since we don't want to change the mode, just trigger
267-
// the logic to update the last connection success.
268287
updateConnectionInfoForSuccess(connectionInformation.getConnectionMode());
269288
onCompletion.onSuccess(null);
270289
}
271290

272291
@Override
273292
public void onError(Throwable error) {
274-
// passing the current connection mode since we don't want to change the mode, just trigger
275-
// the logic to update the last connection failure.
276293
updateConnectionInfoForError(connectionInformation.getConnectionMode(), error);
277294
onCompletion.onSuccess(null);
278295
}
279296
});
280297

281-
// If the app starts in the background, the builder creates the data source with
282-
// STREAMING as the starting mode. Perform an initial mode resolution to correct this.
283-
if (dataSource instanceof ModeAware) {
284-
resolveAndSwitchMode((ModeAware) dataSource);
285-
}
286-
287298
return true;
288299
}
289300

@@ -425,6 +436,13 @@ synchronized boolean startUp(@NonNull Callback<Void> onCompletion) {
425436
return false;
426437
}
427438
initialized = false;
439+
440+
if (useFDv2ModeResolution) {
441+
currentFDv2Mode = resolveMode();
442+
FDv2DataSourceBuilder fdv2Builder = (FDv2DataSourceBuilder) dataSourceFactory;
443+
fdv2Builder.setActiveMode(currentFDv2Mode, true);
444+
}
445+
428446
updateEventProcessor(forcedOffline.get(), platformState.isNetworkAvailable(), platformState.isForeground());
429447
return updateDataSource(true, onCompletion);
430448
}
@@ -476,34 +494,18 @@ private void handleModeStateChange() {
476494
}
477495

478496
/**
479-
* Resolves the current platform state to a ConnectionMode via the ModeResolutionTable,
480-
* looks up the ResolvedModeDefinition from the resolved mode table, and calls
481-
* switchMode() on the data source if the mode has changed.
497+
* Resolves the current platform state to a {@link ConnectionMode} via the
498+
* {@link ModeResolutionTable}.
482499
*/
483-
private void resolveAndSwitchMode(@NonNull ModeAware modeAware) {
484-
Map<ConnectionMode, ResolvedModeDefinition> table = resolvedModeTable;
485-
if (table == null) {
486-
return;
487-
}
500+
private ConnectionMode resolveMode() {
488501
boolean forceOffline = forcedOffline.get();
489502
boolean networkAvailable = platformState.isNetworkAvailable();
490503
boolean foreground = platformState.isForeground();
491504
ModeState state = new ModeState(
492505
foreground && !forceOffline,
493506
networkAvailable && !forceOffline
494507
);
495-
ConnectionMode newMode = ModeResolutionTable.MOBILE.resolve(state);
496-
if (newMode == currentFDv2Mode) {
497-
return;
498-
}
499-
currentFDv2Mode = newMode;
500-
ResolvedModeDefinition def = table.get(newMode);
501-
if (def == null) {
502-
logger.warn("No resolved definition for mode {}; skipping switchMode", newMode);
503-
return;
504-
}
505-
logger.debug("Switching FDv2 mode to {}", newMode);
506-
modeAware.switchMode(def);
508+
return ModeResolutionTable.MOBILE.resolve(state);
507509
}
508510

509511
synchronized ConnectionInformation getConnectionInformation() {

launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2DataSource.java

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* switch to next synchronizer) and recovery (when on non-prime synchronizer, try
3131
* to return to the first after timeout).
3232
*/
33-
final class FDv2DataSource implements DataSource, ModeAware {
33+
final class FDv2DataSource implements DataSource {
3434

3535
/**
3636
* Factory for creating Initializer or Synchronizer instances.
@@ -50,8 +50,6 @@ public interface DataSourceFactory<T> {
5050
private final AtomicBoolean started = new AtomicBoolean(false);
5151
private final AtomicBoolean startCompleted = new AtomicBoolean(false);
5252
private final AtomicBoolean stopped = new AtomicBoolean(false);
53-
private final AtomicBoolean executionLoopRunning = new AtomicBoolean(false);
54-
5553
/** Result of the first start (null = not yet completed). Used so second start() gets the same result. */
5654
private volatile Boolean startResult = null;
5755
private volatile Throwable startError = null;
@@ -140,7 +138,6 @@ public void start(@NonNull Callback<Boolean> resultCallback) {
140138
LDContext context = evaluationContext;
141139

142140
sharedExecutor.execute(() -> {
143-
executionLoopRunning.set(true);
144141
try {
145142
if (!sourceManager.hasAvailableSources()) {
146143
logger.info("No initializers or synchronizers; data source will not connect.");
@@ -167,8 +164,6 @@ public void start(@NonNull Callback<Boolean> resultCallback) {
167164
} catch (Throwable t) {
168165
logger.warn("FDv2DataSource error: {}", t.toString());
169166
tryCompleteStart(false, t);
170-
} finally {
171-
executionLoopRunning.set(false);
172167
}
173168
});
174169
}
@@ -221,32 +216,11 @@ public void stop(@NonNull Callback<Void> completionCallback) {
221216

222217
@Override
223218
public boolean needsRefresh(boolean newInBackground, @NonNull LDContext newEvaluationContext) {
224-
// Mode-aware data sources handle background/foreground transitions via switchMode(),
225-
// so only request a full rebuild when the evaluation context changes.
219+
// FDv2 background/foreground transitions are handled externally by ConnectivityManager
220+
// via teardown/rebuild, so only request a rebuild when the evaluation context changes.
226221
return !evaluationContext.equals(newEvaluationContext);
227222
}
228223

229-
@Override
230-
public void switchMode(@NonNull ResolvedModeDefinition newDefinition) {
231-
List<SynchronizerFactoryWithState> newSyncFactories = new ArrayList<>();
232-
for (DataSourceFactory<Synchronizer> factory : newDefinition.getSynchronizerFactories()) {
233-
newSyncFactories.add(new SynchronizerFactoryWithState(factory));
234-
}
235-
// Per CONNMODE 2.0.1: mode switches only transition synchronizers, no initializers.
236-
sourceManager.switchSynchronizers(newSyncFactories);
237-
238-
sharedExecutor.execute(() -> {
239-
if (!executionLoopRunning.compareAndSet(false, true)) {
240-
return;
241-
}
242-
try {
243-
runSynchronizers(evaluationContext, dataSourceUpdateSink);
244-
} finally {
245-
executionLoopRunning.set(false);
246-
}
247-
});
248-
}
249-
250224
private void runInitializers(
251225
@NonNull LDContext context,
252226
@NonNull DataSourceUpdateSinkV2 sink

launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilder.java

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,10 @@
2525
import java.util.concurrent.ScheduledExecutorService;
2626

2727
/**
28-
* Builds an {@link FDv2DataSource} and resolves the mode table from
29-
* {@link ComponentConfigurer} factories into zero-arg {@link FDv2DataSource.DataSourceFactory}
30-
* instances. The resolved table is stored and exposed via {@link #getResolvedModeTable()}
31-
* so that {@link ConnectivityManager} can perform mode→definition lookups when switching modes.
32-
* <p>
33-
* This is the key architectural difference in Approach 2: the builder owns the resolved
34-
* table rather than the data source itself.
28+
* Builds an {@link FDv2DataSource} by resolving {@link ComponentConfigurer} factories
29+
* into zero-arg {@link FDv2DataSource.DataSourceFactory} instances. The builder is the
30+
* sole owner of mode resolution; {@link ConnectivityManager} configures the target mode
31+
* via {@link #setActiveMode} before calling the standard {@link #build}.
3532
* <p>
3633
* Package-private — not part of the public SDK API.
3734
*/
@@ -40,7 +37,9 @@ class FDv2DataSourceBuilder implements ComponentConfigurer<DataSource> {
4037
private final Map<ConnectionMode, ModeDefinition> modeTable;
4138
private final ConnectionMode startingMode;
4239

43-
private Map<ConnectionMode, ResolvedModeDefinition> resolvedModeTable;
40+
private ConnectionMode activeMode;
41+
private boolean includeInitializers = true; // false during mode switches to skip initializers (CONNMODE 2.0.1)
42+
private ScheduledExecutorService sharedExecutor;
4443

4544
FDv2DataSourceBuilder() {
4645
this(makeDefaultModeTable(), ConnectionMode.STREAMING);
@@ -175,53 +174,64 @@ private static Map<ConnectionMode, ModeDefinition> makeDefaultModeTable() {
175174
this.startingMode = startingMode;
176175
}
177176

178-
/**
179-
* Returns the resolved mode table after {@link #build} has been called.
180-
* Each entry maps a {@link ConnectionMode} to a {@link ResolvedModeDefinition}
181-
* containing zero-arg factories that capture the {@link ClientContext}.
182-
*
183-
* @return unmodifiable map of resolved mode definitions
184-
* @throws IllegalStateException if called before {@link #build}
185-
*/
186177
@NonNull
187178
ConnectionMode getStartingMode() {
188179
return startingMode;
189180
}
190181

191-
@NonNull
192-
Map<ConnectionMode, ResolvedModeDefinition> getResolvedModeTable() {
193-
if (resolvedModeTable == null) {
194-
throw new IllegalStateException("build() must be called before getResolvedModeTable()");
195-
}
196-
return resolvedModeTable;
182+
/**
183+
* Configures the mode to build for and whether to include initializers.
184+
* Called by {@link ConnectivityManager} before each {@link #build} call.
185+
*
186+
* @param mode the target connection mode
187+
* @param includeInitializers true for initial startup / identify, false for mode switches
188+
* (per CONNMODE 2.0.1: mode switches only transition synchronizers)
189+
*/
190+
void setActiveMode(@NonNull ConnectionMode mode, boolean includeInitializers) {
191+
this.activeMode = mode;
192+
this.includeInitializers = includeInitializers;
193+
}
194+
195+
/**
196+
* Returns the raw {@link ModeDefinition} for the given mode, used by
197+
* {@link ConnectivityManager} for the CSFDV2 5.3.8 equivalence check.
198+
*/
199+
ModeDefinition getModeDefinition(@NonNull ConnectionMode mode) {
200+
return modeTable.get(mode);
197201
}
198202

199203
@Override
200204
public DataSource build(ClientContext clientContext) {
201-
Map<ConnectionMode, ResolvedModeDefinition> resolved = new LinkedHashMap<>();
202-
for (Map.Entry<ConnectionMode, ModeDefinition> entry : modeTable.entrySet()) {
203-
resolved.put(entry.getKey(), resolve(entry.getValue(), clientContext));
204-
}
205-
this.resolvedModeTable = Collections.unmodifiableMap(resolved);
205+
ConnectionMode mode = activeMode != null ? activeMode : startingMode;
206206

207-
ResolvedModeDefinition startDef = resolvedModeTable.get(startingMode);
208-
if (startDef == null) {
207+
ModeDefinition modeDef = modeTable.get(mode);
208+
if (modeDef == null) {
209209
throw new IllegalStateException(
210-
"Starting mode " + startingMode + " not found in mode table");
210+
"Mode " + mode + " not found in mode table");
211211
}
212212

213+
ResolvedModeDefinition resolved = resolve(modeDef, clientContext);
214+
213215
DataSourceUpdateSink baseSink = clientContext.getDataSourceUpdateSink();
214216
if (!(baseSink instanceof DataSourceUpdateSinkV2)) {
215217
throw new IllegalStateException(
216218
"FDv2DataSource requires a DataSourceUpdateSinkV2 implementation");
217219
}
218220

219-
ScheduledExecutorService sharedExecutor = Executors.newScheduledThreadPool(2);
221+
if (sharedExecutor == null) {
222+
sharedExecutor = Executors.newScheduledThreadPool(2);
223+
}
224+
225+
List<FDv2DataSource.DataSourceFactory<Initializer>> initFactories =
226+
includeInitializers ? resolved.getInitializerFactories() : Collections.<FDv2DataSource.DataSourceFactory<Initializer>>emptyList();
227+
228+
// Reset includeInitializers to default after each build to prevent stale state.
229+
includeInitializers = true;
220230

221231
return new FDv2DataSource(
222232
clientContext.getEvaluationContext(),
223-
startDef.getInitializerFactories(),
224-
startDef.getSynchronizerFactories(),
233+
initFactories,
234+
resolved.getSynchronizerFactories(),
225235
(DataSourceUpdateSinkV2) baseSink,
226236
sharedExecutor,
227237
clientContext.getBaseLogger()

launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ModeAware.java

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)