Skip to content

Aaronz/sdk 1956/mode resolution and switching#326

Draft
aaron-zeisler wants to merge 13 commits intomainfrom
aaronz/SDK-1956/mode-resolution-and-switching
Draft

Aaronz/sdk 1956/mode resolution and switching#326
aaron-zeisler wants to merge 13 commits intomainfrom
aaronz/SDK-1956/mode-resolution-and-switching

Conversation

@aaron-zeisler
Copy link

Details coming soon

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

Provide links to any issues in this repository or elsewhere relating to this pull request.

Describe the solution you've provided

Provide a clear and concise description of what you expect to happen.

Describe alternatives you've considered

Provide a clear and concise description of any alternative solutions or features you've considered.

Additional context

Add any other context about the pull request here.

tanderson-ld and others added 13 commits February 24, 2026 17:27
Introduces the core types for FDv2 mode resolution (CONNMODE spec):
- ConnectionMode: enum for streaming, polling, offline, one-shot, background
- ModeDefinition: initializer/synchronizer lists per mode with stubbed configurers
- ModeState: platform state snapshot (foreground, networkAvailable)
- ModeResolutionEntry: condition + mode pair for resolution table entries
- ModeResolutionTable: ordered first-match-wins resolver with MOBILE default table
- ModeAware: interface for DataSources that support runtime switchMode()

All types are package-private. No changes to existing code.
Add ResolvedModeDefinition and mode-table constructors so
FDv2DataSource can look up initializer/synchronizer factories per
ConnectionMode. switchMode() tears down the current SourceManager,
builds a new one with the target mode's synchronizers (skipping
initializers per CONNMODE 2.0.1), and schedules execution on the
background executor. runSynchronizers() now takes an explicit
SourceManager parameter to prevent a race where the finally block
could close a SourceManager swapped in by a concurrent switchMode().
Introduces FDv2DataSourceBuilder, a ComponentConfigurer<DataSource> that
resolves the ModeDefinition table's ComponentConfigurers into
DataSourceFactories at build time by capturing the ClientContext. The
configurers are currently stubbed (return null); real wiring of concrete
initializer/synchronizer types will follow in a subsequent commit.
…ourceBuilder

Replace stub configurers with concrete factories that create
FDv2PollingInitializer, FDv2PollingSynchronizer, and
FDv2StreamingSynchronizer. Shared dependencies (SelectorSource,
ScheduledExecutorService) are created once per build() call; each
factory creates a fresh DefaultFDv2Requestor for lifecycle isolation.

Add FDv2 endpoint path constants to StandardEndpoints. Thread
TransactionalDataStore through ClientContextImpl and ConnectivityManager
so the builder can construct SelectorSourceFacade from ClientContext.
ConnectivityManager now detects ModeAware data sources and routes
foreground, connectivity, and force-offline state changes through
resolveAndSwitchMode() instead of the legacy teardown/rebuild cycle.
@@ -111,6 +140,74 @@ public interface DataSourceFactory<T> {
this.sharedExecutor = sharedExecutor;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can get rid of the old constructors since it will only ever be used in a mode switching setup.

* @see ConnectionMode
* @see ModeResolutionTable
*/
interface ModeAware extends DataSource {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider not extending here if not necessary.

} else if (dataSource == null || dataSource.needsRefresh(!foreground,
currentContext.get())) {
updateDataSource(true, LDUtil.noOpCallback());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update each platform state listener to call handleModeState(...). Inside handleModeState, decide if you need to change the eventProcessor state and decide if you need to change the data source state.

connectivityChangeListener = networkAvailable -> {
    handleModeStateChange()
}

foregroundListener = foreground -> {
    handleModeStateChange()
}


handleModeStateChange() {
    ModeState state = new ModeState(
        platformState.isForeground(),
        platformState.isNetworkAvailable()
    )

    updateEventProcessor(state)
    updateDataSource(state)
}

updateEventProcessor(newModeState) {
    eventProcessor.setOffline(forcedOffline.get() || !networkAvailable);
    eventProcessor.setInBackground(!foreground);
}

updateDataSource(newModeState) {
    if (dataSource instanceof ModeAware) {
        resolveAndSwitchMode((ModeAware) dataSource, newModeState);
    } else {
        updateDataSource(false, LDUtil.noOpCallback());
    }
}

LDContext context = currentContext.get();

eventProcessor.setOffline(forceOffline || !networkEnabled);
eventProcessor.setInBackground(inBackground);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This event processor logic should be moved out of update datasource.

this.evaluationContext = evaluationContext;
this.dataSourceUpdateSink = dataSourceUpdateSink;
this.logger = logger;
this.modeTable = Collections.unmodifiableMap(new EnumMap<>(modeTable));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably shouldn't make a copy. I would expect the modeTable to already be an immutable.

this.evaluationContext = evaluationContext;
this.dataSourceUpdateSink = dataSourceUpdateSink;
this.logger = logger;
this.modeTable = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider Collections.emptyMap() iirc.


ResolvedModeDefinition startDef = modeTable.get(startingMode);
if (startDef == null) {
throw new IllegalArgumentException("No mode definition for starting mode: " + startingMode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a runtime exception, it would be better to log an error. A future call to switchMode could possible be valid.

Eventually the starting mode will be coming from the customer, so there may be cases where they can provide invalid inputs if we don't constrain them with a Builder pattern or such. It seems like in any case where invalid configuration input was provided, the SDK could just be offline and not making connections.

Copy link
Author

@aaron-zeisler aaron-zeisler Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: Look at line 240 to understand what happens if the SourceManager has no initializers or synchronizers.

logger.warn("switchMode({}) called but no mode table configured", newMode);
return;
}
if (stopped.get()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stopped check probably would be before modeTable check.

*/
@Override
public void switchMode(@NonNull ConnectionMode newMode) {
if (modeTable == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may turn into a empty check instead of a null check due to my emptyMap comment above.

sourceManager = newManager;
if (oldManager != null) {
oldManager.close();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look into race condition. There may already be an execution thread interacting with the old manager. Is it ok to start the next execution task before the previous one finishes? Probably not to avoid an out of order push of data into the update sink.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can perform the swap inside SourceManager. SourceManager already has utilities to manage synchronizers in a thread-safe manner. Idea: We add a switchMode() method to SourceManager. The benefit is then the DataSource must work w/ the SourceManager to retrieve synchronizers via getNextInitializerAndSetActive(), so SourceManager can swap them under the hood.

) {
try {
Synchronizer synchronizer = sourceManager.getNextAvailableSynchronizerAndSetActive();
Synchronizer synchronizer = sm.getNextAvailableSynchronizerAndSetActive();
Copy link
Author

@aaron-zeisler aaron-zeisler Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: This could block until a valid synchronizer is available. This is in reference to the comment around startDef being invalid.

Needs more discussion, to be continued tomorrow.

Base automatically changed from ta/SDK-1835/initializers-synchronizers to main March 13, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants