Repository navigation
feat: [SDK-5581] gate dispatcher bootstrap behind sdk_dispatcher_bootstrap - #2805
Conversation
…strap Keep the lazy pools in the binary so the unreleased bootstrap path stays off until Turbine enables it. The choice is latched from the cached feature list before FeatureManager exists. Co-authored-by: Cursor <cursoragent@cursor.com>
Read the cached flag before taking the lock, and resolve an application when startup did not record one, so a cold receiver can still select bootstrap. Log the on or off decision to logcat, and keep lazy prewarm from building executors after shutdown. Co-authored-by: Cursor <cursoragent@cursor.com>
📊 Diff Coverage ReportDiff Coverage Report (Changed Lines Only)Gate: aggregate coverage on changed executable lines must be ≥ 80% (JaCoCo line data for lines touched in the diff). Changed Files Coverage
Overall (aggregate gate)476/521 touched executable lines covered (91.4% — requires ≥ 80%) Per-file detail (informational; gate is aggregate above):
|
There was a problem hiding this comment.
Stale comment
Multi-model review (Opus 5.5, GPT 5.6 Sol, Grok 4.7)
Intent: latch
sdk_dispatcher_bootstrapfrom the cached feature list before FeatureManager exists, default lazy, so PR 2766 can ramp.Act on
Caller-thread prefs I/O on first latch (3/3).
prewarm()/launchOn*→currentPools()→DispatcherBootstrapCache.read()doesgetSharedPreferences("OneSignal").getString(...)on the caller. That is the androidx.startup thread,initWithContext, and receivers beforegoAsync(). Moving the read outsidepoolsLockdoes not keep it off those threads. Default-off is a new main-thread disk read vs shipped 5.10.2, and flag-on still blocks the firstlaunchOn*on XML load — the opposite of the bootstrap goal.Consider
- Lazy prewarm cannot retry after
Thread.start()fails (3/3).compareAndSetpublishes the thread beforestart(). The outer catch clearsprewarmStartedbut notprewarmThread, so laterprewarm()returns without warming.- Unreadable config logs the payload (Grok, verified). Android
JSONTokenerincludes the fullMODEL_STORE_configstring inJSONException; that document can holdfcmParams.apiKey.- Context already in hand is unused (Opus).
initWithContextnever callsnoteApplicationContext, so a disabled startup provider falls through to hiddenActivityThread.currentApplication.- KMP pin is the pre-squash SHA (Opus). #28 is merged;
mainis53e568c. Pin is still4d26557.Noted
Test isolation leak of a mock
Applicationvia the realnoteApplicationContext; production latch / race-losershutdown()is not exercised (resetForTestinjects pools); FeatureManager/local overrides can disagree with the latched mode (telemetry not attached yet); fallback lazy lanes reportActive.Dismissed
logReleasebypassingOneSignal.Debug.logLevel— intentional so release (NONE) still shows the on/off line, andLoggingcan dispatch.Sent by Cursor Automation: PR Reviews
Clear the prewarm thread if start fails, log only the exception type when the config store will not parse, and record the init context before warmup. Pin the catalog at the merged KMP commit. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Fixed in bdd9206, except the prefs read.
|
|
Potential issues:
|
- OneSignalDispatchers reads sdk_dispatcher_bootstrap through FeatureManager.startupFlags instead of its own cache, so the first caller never loads the OneSignal prefs file on main and FeatureManager reports the pools the process actually latched. - Drop DispatcherBootstrapCache. - A closed lazy lane reports Shutdown. - Run the shared lane behavior tests against both pool types, and cover an unreadable startup file. Co-authored-by: Cursor <cursoragent@cursor.com>
…hoice Use StartupFlagsSource and report a missing cache as "off, no cache yet". Co-authored-by: Cursor <cursoragent@cursor.com>
- Lane and pool failures go through Logging.warn again, so they reach remote telemetry. A per-thread guard stops a log listener that launches onto the failing lane from re-entering the failure log. - A failed pool choice falls back to lazy pools instead of throwing from every launch. - Tests reset the remembered application context between specs, so one spec's mock Application cannot pick the next spec's pools. - Split the bootstrap pools out of OneSignalDispatchers.kt. Co-authored-by: Cursor <cursoragent@cursor.com>
9d8fcc9 to
5775d3d
Compare
…emetry An unreadable startup file, no application context, or a failed choice now reaches Logging.warn once init has installed remote telemetry, via DispatcherChoiceReporter. Pools are usually chosen before that, so the report waits for it and later choices report right away. Also documents and tests that a choice failure after an on snapshot leaves FeatureManager reporting the flag on over lazy pools. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Fix CI |
|
@fadi-george thanks. Status of each:
|
Its a flaxy test in OperationsRepo. We need to work on this seperately. |
…priority Main can block on both: waiting for init, which queues behind the bootstrap builds, or on a lazy lane the prewarm thread is building. At NORM_PRIORITY - 2 a busy device could starve them while main waited. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Multi-model review (Opus 5.5, GPT 5.6 Sol, Grok 4.7)
Intent: latch sdk_dispatcher_bootstrap from the startup-flags snapshot (default lazy) so PR 2766 can ramp, and report degraded choices once init can see them.
Act on
Degraded-choice telemetry can be dropped (2/3 Opus+Sol). recordChoice and reportChoiceToTelemetry hand off across two volatiles. A chooser that reads telemetryReady == false can write pendingChoiceReport after the reporter has already drained it. That is the only remote event this commit adds.
Consider
- Re-entrant
kotlin.lazyfromlogFailure(2/3 Opus+Grok).Logging.warncanlaunchOn*on the same thread; the inner initializer can publish an executor that the outer catch then overwrites withnull. That pool is nevershutdownNow()and hasallowCoreThreadTimeOut(false). - First-caller snapshot read is still on main (2/3, accepted). It is now the small
OneSignalStartupFlagsfile, not the full OneSignal prefs store. Residual StrictMode/disk-read cost is documented.
Noted
- Lazy
shutdown()can miss an initializer that has passed its lastclosedcheck but not yet published (Sol). - A failed bootstrap
Thread.startleavesprewarmStartedtrue; a laterdispatchstill retries the gate (Grok). - A
NO_STOREfirst snapshot (receiverprewarm+ failedActivityThreadreflection) latches every APP_STARTUP flag off for the process (Opus).
Dismissed
Log.iatLogLevel.NONE— intentional for the ramp.- Lazy
prewarm()then immediatelaunchOn*constructing on the caller — documented flag-off behavior. - Prior findings (prewarm start retry, config payload in logs,
initWithContextcontext) — fixed.
Sent by Cursor Automation: PR Reviews
|
Some findings:
|
- The pool choice and failure lines no longer print when the customer's log level is below them. - Use the shared currentApplication() fallback from the startup flags cache. - Move tests to the supplier write. Co-authored-by: Cursor <cursoragent@cursor.com>
ar/startup-flags replaced the ActivityThread.currentApplication reflection with the AppContext holder, so the dispatcher side follows: - OneSignalDispatchers drops its own appContext field, noteApplicationContext and the reflection fallback; the pool choice reads AppContext. - prewarm takes the entry point's Context and notes it before warming, so receivers, services and activities that run before init still latch from the startup flags file. - With no context noted, the pools stay lazy rather than guessing one. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Minor:
|


Description
One Line Summary
Gate dispatcher bootstrap behind the
sdk_dispatcher_bootstrapstartup flag, default off, and log the on/off choice.Details
Motivation
PR 2766 moves pool creation off the caller thread. That change has not shipped. This keeps the old lazy pools in the binary and turns bootstrap on only when the startup flag is set, so the first release can ramp it.
Ticket: SDK-5581
Stacked on #2808 (startup flags file and snapshot), which depends on KMP #29. Catalog key: KMP #28.
Scope
launchOn*andprewarmdo not see the flag.FeatureManager.startupFlags { context }, the same per-process snapshotFeatureManager.isEnabledreports, so telemetry never shows the flag on while the pools are lazy.OneSignal-bootstrap). Off, no startup file yet, unreadable file, or no application: lazy pools. A failed choice also falls back to lazy pools instead of throwing from every launch.prewarm(context)notes the entry point's context inAppContext(from feat: [SDK-5581] latch APP_STARTUP flags from a small startup file #2808) before warming, so receivers, services and activities that run before init latch from the file. If nothing noted a context, this process stays on lazy pools.APP_STARTUP: a value fetched this process applies on the next start. First launch with no startup file stays lazy.OneSignaland prefixsdk_dispatcher_bootstrap(on; pools=bootstrap,off, flag not in cache; pools=lazy,off, no cache yet; pools=lazy,off, cache unreadable; pools=lazy,off, no application; pools=lazy,off, choice failed; pools=lazy).Logging.warn. Pools are usually chosen before init installs telemetry, so the report waits forDispatcherChoiceReporter, a startable service, and choices made after that report right away.Logging.warn. A per-thread guard stops a log listener that launches onto the failing lane from re-entering the failure log.FeatureManager.isEnabledstill reports on. Theoff, choice failedreport is what separates those launches in telemetry.BootstrapDispatcherPools.ktsoOneSignalDispatchers.ktstays under 500 lines (378). No behavior change from the move.No public API change. The flag is not removed in this PR.
Testing
Unit testing
DispatcherLaneBehaviorTests: every lane behavior test runs with bootstrap pools and with lazy pools.DispatcherBootstrapFlagTests: flag key selects bootstrap, no key and unreadable file stay on the caller, a failed choice falls back to lazy pools, the pools latch from the context passed to prewarm, no noted context stays lazy, andFeatureManagerreports the pools actually latched.FeatureManagerreports on, an unreadable file reaches telemetry once init installs it (and only once), a degraded choice after that reports right away, and a healthy choice is not reported. Verified red with the report reverted.DispatcherFailureLogTests: lane failures reach log listeners in both modes, a listener that launches work does not re-enter lane creation, and logcat lines follow the SDK log level.OneSignalDispatchersTestsandActivityLifecycleInitializerTestscover the mode-specific paths. Tests reset the remembered application context between specs../gradlew :OneSignal:core:spotlessCheck :OneSignal:core:detekt :OneSignal:core:testReleaseUnitTestpasses.Manual testing
Emulator: Medium Phone, API 36, demo
com.onesignal.example. Run on an earlier revision of this branch; the pool choice behavior is unchanged since.sdk_dispatcher_bootstrap: on; pools=bootstrapon the main thread. Process stayed up. Bootstrap worker threads appeared.sdk_dispatcher_bootstrap: off, flag not in cache; pools=lazy. Init completed.launchOnIOon the main thread and the SDK watchdog loggedANR detected! Main thread unresponsive for 6003ms (foreground). Flag on ran the same stall onOneSignal-bootstrapandlaunchOnIOreturned in 0ms with no ANR line.Affected code checklist
Checklist
Overview
Testing
Final pass