Aaronz/sdk 1956/mode resolution and switching#326
Aaronz/sdk 1956/mode resolution and switching#326aaron-zeisler wants to merge 13 commits intomainfrom
Conversation
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; | |||
| } | |||
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Consider not extending here if not necessary.
| } else if (dataSource == null || dataSource.needsRefresh(!foreground, | ||
| currentContext.get())) { | ||
| updateDataSource(true, LDUtil.noOpCallback()); | ||
| } |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Consider Collections.emptyMap() iirc.
|
|
||
| ResolvedModeDefinition startDef = modeTable.get(startingMode); | ||
| if (startDef == null) { | ||
| throw new IllegalArgumentException("No mode definition for starting mode: " + startingMode); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
stopped check probably would be before modeTable check.
| */ | ||
| @Override | ||
| public void switchMode(@NonNull ConnectionMode newMode) { | ||
| if (modeTable == null) { |
There was a problem hiding this comment.
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(); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
Details coming soon
Requirements
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.