feat: SDK-4176: gate background threading behind remote feature flag#2595
feat: SDK-4176: gate background threading behind remote feature flag#2595abdulraqeeb33 merged 3 commits intomainfrom
Conversation
Add a generic feature-manager pipeline backed by ConfigModel.features and gate core startup/init/threading behavior behind BACKGROUND_THREADING, with next-run latching and test coverage for both FF-on and FF-off execution paths. Made-with: Cursor
There was a problem hiding this comment.
Pull request overview
This PR introduces a remote-config–driven feature-flag mechanism (ConfigModel.features) and uses it to gate a new background-threading execution model behind the BACKGROUND_THREADING flag, while attempting to preserve legacy behavior when the flag is off.
Changes:
- Add feature-flag plumbing (
ConfigModel.features, backend parsing,FeatureManager,FeatureFlag) and a globalThreadingModeswitch. - Gate core startup + threading helpers (startup scheduling, thread utils, crash uploader, HTTP/prefs helpers) based on
BACKGROUND_THREADING. - Add/adjust unit tests to validate feature-flag ON/OFF behavior for FeatureManager, ThreadUtils, startup scheduling, and crash uploader wrapper.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/ConfigModel.kt |
Adds features: List<String> and JSON list deserialization support. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/IParamsBackendService.kt |
Extends ParamsObject to include features. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/backend/impl/ParamsBackendService.kt |
Parses features array from backend response. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/config/impl/ConfigModelStoreListener.kt |
Hydrates ConfigModel.features from backend params. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureFlag.kt |
Defines feature flags + activation mode (RUNTIME vs NEXT_RUN). |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/features/FeatureManager.kt |
Implements feature evaluation and applies threading side-effects. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadingMode.kt |
Adds global switch for background-threading mode. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt |
Gates coroutine/dispatcher usage vs legacy execution based on ThreadingMode. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/startup/StartupService.kt |
Gates service start scheduling between OneSignal dispatchers and legacy threads. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapper.kt |
Gates crash uploader startup threading strategy behind the feature flag. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/preferences/impl/PreferencesService.kt |
Routes async loop to launchOnIO and adds extra logging/guardrails. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/http/impl/HttpClient.kt |
Switches IO execution to launchOnIO helper. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt |
Gates init/login/logout and service getters based on background-threading flag. |
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/CoreModule.kt |
Registers FeatureManager as IFeatureManager. |
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/features/FeatureManagerTests.kt |
Adds tests for NEXT_RUN behavior and threading-mode side effects. |
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/threading/ThreadUtilsFeatureFlagTests.kt |
Adds tests for launchOnIO/Default gating behavior. |
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/startup/StartupServiceTests.kt |
Updates startup tests to inject a mocked IFeatureManager and adds OFF-path test. |
OneSignalSDK/onesignal/core/src/test/java/com/onesignal/debug/internal/crash/OneSignalCrashUploaderWrapperTest.kt |
Updates tests for new crash uploader wrapper constructor dependency on IFeatureManager. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| val prefsToWrite = getSharedPrefsByName(storeKey) | ||
|
|
||
| if (prefsToWrite == null) { | ||
| // the assumption here is there is no context yet, but will be. So ensure | ||
| // we wake up to try again and persist the preference. | ||
| if (!hasLoggedMissingAppContext) { | ||
| Logging.warn("OneSignal: PreferencesService app context unavailable, deferring writes") | ||
| hasLoggedMissingAppContext = true | ||
| } | ||
| waiter.wake() | ||
| continue |
There was a problem hiding this comment.
prefsToWrite == null branch is effectively unreachable with the current getSharedPrefsByName implementation (it dereferences appContext with !! and always returns a SharedPreferences). If the app context is actually unavailable, this code will crash with an NPE before reaching the intended deferral logic/logging. Consider making getSharedPrefsByName safely return null when appContext isn't initialized yet (e.g., catch the NPE/IllegalStateException), or remove the null-handling path here if it can never happen by design.
| if (isBackgroundThreadingEnabled) { | ||
| // init in background and return immediately to ensure non-blocking | ||
| suspendifyOnIO { | ||
| internalInit(context, appId) | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| return runBlocking(runtimeIoDispatcher) { | ||
| internalInit(context, appId) | ||
| } |
There was a problem hiding this comment.
When BACKGROUND_THREADING is disabled, initWithContext now runs internalInit inside runBlocking(...), which blocks the calling thread until initialization completes. Since OneSignal.initWithContext(...) is commonly called from Application.onCreate (main thread), this can regress startup responsiveness and risk ANRs. Consider keeping initWithContext non-blocking regardless of the feature flag (dispatch init work to a background thread/dispatcher) and only gate the internal dispatcher/threading model behind the feature flag.
| // Then | ||
| Thread.sleep(50) |
There was a problem hiding this comment.
This test relies on Thread.sleep(50) to wait for the background Thread { ... } path, which is timing-dependent and can be flaky on slower CI machines. Prefer a deterministic synchronization primitive (e.g., CountDownLatch, CompletableDeferred, or polling verify(timeout = ...) if available) to wait until both start() calls have happened.
| // Then | |
| Thread.sleep(50) | |
| // Then - wait deterministically for any async work to complete | |
| awaitIO() |
| @@ -46,15 +50,29 @@ internal class OneSignalCrashUploaderWrapper( | |||
| @Suppress("TooGenericExceptionCaught") | |||
| override fun start() { | |||
There was a problem hiding this comment.
otel should use feature check too?
There was a problem hiding this comment.
we already do
we short circuit here
Make PreferencesService context lookup safely nullable for early startup, replace timing-based startup test waiting with deterministic latch synchronization, and document intentional legacy blocking behavior when BACKGROUND_THREADING is off. Made-with: Cursor
Summary
ConfigModel.featuresand introduceBACKGROUND_THREADINGas a next-run feature.FeatureManagerTests,ThreadUtilsFeatureFlagTests,StartupServiceTests, crash uploader test updates).Test plan
./gradlew --console=plain --no-daemon :onesignal:core:testDebugUnitTest --tests "com.onesignal.core.internal.features.FeatureManagerTests" --tests "com.onesignal.common.threading.ThreadUtilsFeatureFlagTests" --tests "com.onesignal.core.internal.startup.StartupServiceTests"./gradlew --console=plain --no-daemon :onesignal:core:testDebugUnitTest --tests "com.onesignal.internal.OneSignalImpTests" --tests "com.onesignal.core.internal.preferences.PreferencesServiceTests"FeatureManagerallows forcingBACKGROUND_THREADINGfor device-side verification.Made with Cursor