chore: clearing selector and fixing fdv2 race conditions#347
chore: clearing selector and fixing fdv2 race conditions#347tanderson-ld merged 4 commits intomainfrom
Conversation
| private final SelectorSource selectorSource; | ||
|
|
||
| /** Used by FDv1 code paths that do not need a {@link TransactionalDataStore}. */ | ||
| /** Used by FDv1 code paths that do not need a {@link SelectorSource}. */ |
There was a problem hiding this comment.
For reviewers: The transactional data store was more than strictly necessary, so tidying that up.
| private final ConnectionInformationState connectionInformation; | ||
| private final PersistentDataStoreWrapper.PerEnvironmentData environmentStore; | ||
| private final EventProcessor eventProcessor; | ||
| private final TransactionalDataStore transactionalDataStore; |
There was a problem hiding this comment.
For reviewers: Writing data is now done through the ContextDataManager.ContextDataManagerView
| this.taskExecutor = ClientContextImpl.get(clientContext).getTaskExecutor(); | ||
| this.logger = clientContext.getBaseLogger(); | ||
|
|
||
| currentContext.set(clientContext.getEvaluationContext()); |
There was a problem hiding this comment.
For reviewers: The context is now given to the ConnectivityManager via onContextChanged which fires during startup to set the initial context.
| this.currentContext.set(context); | ||
| this.currentView = view; | ||
|
|
||
| if (oldContext == context || oldContext.equals(context)) { |
There was a problem hiding this comment.
For reviewers: A nice consequence of these changes is that the ConnectivityManager gets dumber. It respects whatever context the ContextDataManager sends via onContextChanged and does not do any checks against it. "Oh, you want a data source for that context, coming right up!"
| } | ||
|
|
||
| private synchronized boolean updateDataSource( | ||
| private boolean updateDataSource( |
There was a problem hiding this comment.
For reviewers: This synchronization was unnecessary. Sync on private methods is usually a smell.
| updateConnectionInfoForSuccess(ConnectionInformation.ConnectionMode.OFFLINE); | ||
| } else if (!newState.isForeground() && newState.isBackgroundUpdatingDisabled()) { | ||
| dataSourceUpdateSink.setStatus(ConnectionInformation.ConnectionMode.BACKGROUND_DISABLED, null); | ||
| updateConnectionInfoForSuccess(ConnectionInformation.ConnectionMode.BACKGROUND_DISABLED); |
There was a problem hiding this comment.
For reviewers: Instead of going through sink impl and then back to a private method of this class, just go straight to the private method in this class.
|
|
||
| ModeState state = snapshotModeState(); | ||
| updateEventProcessor(forcedOffline.get(), state.isNetworkAvailable(), state.isForeground()); | ||
| return updateDataSource(true, state, onCompletion); |
There was a problem hiding this comment.
For reviewers: This logic moved to onContextChanged.
| * | ||
| * @param context to swtich to | ||
| * @param onCompletion callback that indicates when the switching is done | ||
| */ |
There was a problem hiding this comment.
For reviewers: switchToContext no longer exists, instead the connectivity manager listens to synchronous context changes from the ContextDataManager. This allows the context data manager to have confidence views have been invalidated and disseminated in an atomic way.
There was a problem hiding this comment.
Wow! This is a cool change that I wouldn't have thought of 👍
There was a problem hiding this comment.
For reviewers: Not required anymore
| * @param newData the new flag data | ||
| */ | ||
| public void initData( | ||
| @VisibleForTesting |
There was a problem hiding this comment.
Nice, I wasn't aware of this annotation. Is the function still technically public?
There was a problem hiding this comment.
This is an Android annotation and doesn't have any impact on visibility or behavior at runtime. Just informative. Tools can sometimes pick up on the annotations to provide compile time errors and such.
The visibility actually went down as it is no longer public. Default visibility is package private, and since the tests are compiled as part of the same package, the test can see it.
| public void setContextSwitchListener(@NonNull ContextSwitchListener listener) { | ||
| this.contextSwitchListener = listener; | ||
| listener.onContextChanged(currentContext, currentView, LDUtil.noOpCallback()); | ||
| } |
There was a problem hiding this comment.
Should this use the synchronized keyword? Or is it within a lock because it's only called from startUp()?
There was a problem hiding this comment.
Good eye. This is intentionally not in a lock and matches switchToContext. At the moment ConnectivityManager is a well behaved listener and does its tasks quickly and so it can be synchronous and inside the lock.
But I wrote this not assuming the listener is well behaved and I didn't want the listeners work to hold the lock.
aaron-zeisler
left a comment
There was a problem hiding this comment.
This looks good 👍 I have one non-blocking question about using the synchronized keyword in setContextSwitchListener().
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6fa3f6b. Configure here.

Requirements
Related issues
SDK-2072
Describe the solution you've provided
As I was working on the clearing selector story, I noticed we had a race condition that is reachable in FDv2. This race condition did not manifest itself in FDv1 due to version checking. This version checking does not exist in FDv2.
The race condition is that a data source that is in the middle of shutting down may still write data onto the context in the case of rapid switch from Context A -> Context B -> Context A. We thought the context equality checks were sufficient, but as you can see, Context A (first) would equal Context A (last).
The solution to this is to make it such that the ContextDataManager hands out ContextDataManagerViews to the ConnectivityManager along with the context that the ConnectivityManager will use. When data arrives, that data flows through the ContextDataManagerView.
When a context change occurs, the ContextDataManagerView is atomically invalidated before the next context is active, ensuring that no previous datasource can write data.
Note
Medium Risk
Touches core context-switching and data-source lifecycle wiring; mistakes could cause missed updates or identify/startup callbacks not firing, especially under rapid context changes in FDv2.
Overview
Prevents FDv2 race conditions during rapid context switches by introducing
ContextDataManager.ContextDataManagerViewobjects that are invalidated on every context change, causing old data sources to silently drop writes and return an empty selector.Refactors
ConnectivityManagerto register as aContextSwitchListener, rebuild data sources inonContextChanged, and route all data updates through the current view while passing that view throughClientContextImplas aSelectorSource(replacingTransactionalDataStore/SelectorSourceFacade, which is removed). Startup/identify flows are updated to use the new callback-composition andswitchToContext(..., onCompletion)semantics, with expanded test coverage for view invalidation and callback behavior.Reviewed by Cursor Bugbot for commit e6caba2. Bugbot is set up for automated code reviews on this repo. Configure here.