Skip to content

refactor: [SDK-5581] split OneSignalDispatchers for readability and testability - #2809

Merged
abdulraqeeb33 merged 19 commits into
ar/sdk-5581from
ar/sdk-5581-readability
Oct 22, 2026
Merged

abdulraqeeb33 merged 19 commits into
ar/sdk-5581from
ar/sdk-5581-readability

Conversation

@abdulraqeeb33

@abdulraqeeb33 abdulraqeeb33 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

One line summary

Split OneSignalDispatchers.kt into focused files and gather its test hooks into one object. No behavior change except one latch fix and one allocation fix.

Details

Stacked on #2805. Each commit stands alone and can be reviewed on its own.

Why

What

  1. 0354c92b3 Each lane is defined once in DispatcherLanes.kt, shared by both pool modes.
  2. 7d58f125b The six test hooks move into DispatcherTestHooks.kt. They are still read on the production path, and are null outside tests.
  3. c7332599e Pools are chosen under the pools lock, and a degraded choice is logged only after publishing. Before, that log could launch work that re-entered the latch and built a second set of pools. The test in DispatcherFailureLogTests drives this through the telemetry report path and fails (stack overflow) when the report moves back before publishing.
  4. 5ccab9251 Comments trimmed to constraints.
  5. f5c12ad6c GateDispatcher.dispatch returns one decision instead of three mutable flags. dc5e27ea7 builds the Run decision once per READY transition, so a ready dispatch allocates nothing.
  6. 9d8fcc9c9 Diagnostics and pool choice move to DispatcherDiagnostics.kt and PoolChoice.kt.

The re-entry test uses unpublishPoolsForTest() instead of reflecting on poolsRef.

If a pool choice ever re-enters (the pools lock is reentrant), the pools the inner call published are kept and the outer choice is shut down, as on #2805. A test in DispatcherFailureLogTests re-enters from beforePoolChoice and fails without the guard.

Result
OneSignalDispatchers.kt is 206 lines: the public launch API, the latch, and the choice report. The threading package goes from 1,427 lines at the head of #2805 to 1,336.

Testing

  • The 16 lane behavior tests run against both pool modes.
  • Core (1,308) and notifications (246) suites pass at the head. Commits 1 to 5 each passed the full core suite.

AR Abdul Azeez and others added 6 commits October 21, 2026 13:22
Both pool modes now read lane sizes, queue caps, thread names, priorities and fallbacks from one Lane enum and build executors through one newLaneExecutor. Bootstrap still prestarts core threads; lazy does not.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Concurrent first callers already serialize on the StartupFlagsLatch lock, so choosing outside poolsLock unblocked nobody and needed a build-then-discard path. Pools are now chosen once under the lock. A choice failure is logged after publishing, so a log listener that launches work reuses the published pools instead of re-entering the choice.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…lDispatchers

Status, metrics and awaitReadyForTest move to DispatcherDiagnostics.kt as extensions; choosing pools from the startup flags moves to PoolChoice.kt. OneSignalDispatchers keeps the launch API, the pools latch and failure logging.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abdulraqeeb33
abdulraqeeb33 requested a review from a team October 21, 2026 18:51
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

📊 Diff Coverage Report

Diff 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

  • BootstrapDispatcherPools.kt: 25/25 touched executable lines (100.0%) (44 touched lines in diff)
  • DispatcherDiagnostics.kt: 44/45 touched executable lines (97.8%) (81 touched lines in diff)
  • DispatcherLanes.kt: 31/32 touched executable lines (96.9%) (67 touched lines in diff)
  • ⚠️ DispatcherTestHooks.kt: Not in coverage report (may not be compiled/tested)
  • LazyDispatcherPools.kt: 18/18 touched executable lines (100.0%) (19 touched lines in diff)
  • OneSignalDispatchers.kt: 20/20 touched executable lines (100.0%) (35 touched lines in diff)
  • PoolChoice.kt: 23/24 touched executable lines (95.8%) (47 touched lines in diff)

Overall (aggregate gate)

161/164 touched executable lines covered (98.2% — requires ≥ 80%)

📥 View workflow run

AR Abdul Azeez and others added 2 commits October 21, 2026 14:22
Degraded pool choices now go through the report path after publishing.
The re-entry test drives that path, and uses unpublishPoolsForTest
instead of reflecting on poolsRef.

Co-authored-by: Cursor <cursoragent@cursor.com>
The Run decision is built once when a lane turns READY and cleared on
close, so the dispatch hot path allocates nothing.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abdulraqeeb33
abdulraqeeb33 marked this pull request as ready for review October 21, 2026 19:57
Co-authored-by: Cursor <cursoragent@cursor.com>
@fadi-george
fadi-george added this pull request to stack #2810 October 21, 2026 20:20

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Multi-model review (Opus 5.5, GPT 5.6 Sol, Grok 4.7)

Lane settings match the old dual copies. Cached GateDecision.Run is only set with READY and cleared on close. Post-publish logging is the right place for listener re-entry.

Act on

  • publishFirstPools writes poolsRef only after choosePools returns, and poolsLock is reentrant. A same-thread launch during choice publishes generation B, then the outer .also { poolsRef = it.pools } replaces it with A and leaks B. The base publish checked existing != null and shutdown() the loser. The new listener test only launches from recordChoice, which already ran after publish on the base, so it does not pin this window.

Consider

  • First-use flag read (StartupFlagsCache / SharedPreferences) now holds poolsLock. The old comment kept that I/O outside the lock so other launchOn* callers were not stuck behind it. Loser pools were never started, so widening the lock is not what prevents a second live set.

Noted

  • recordChoice / reportChoiceToTelemetry still have a pre-existing check-then-set that can drop a degraded report (Opus + Sol).
  • unpublishPoolsForTest does not reset prewarmStarted or pending telemetry (Grok).
  • choosePools now snapshots bootstrapEnabledForTest before beforePoolChoice (Grok). No current test relies on the old order.

Dismissed

  • run!! NPE on READY: READY is only set together with run.
  • Per-dispatch allocation: the READY path already allocated nothing on the base.
Open in Web View Automation 

Sent by Cursor Automation: PR Reviews

AR Abdul Azeez and others added 5 commits October 22, 2026 08:54
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/OneSignalDispatchers.kt
#	OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/threading/DispatcherBootstrapFlagTests.kt
…enters

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Adapt to the readability split: the choice handoff hook lives in DispatcherTestHooks, and the Fallback readiness check moves to DispatcherDiagnostics.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fadi-george

fadi-george commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

LGTM, nothing blocking:

  • Non-blocking: OneSignalDispatchers.kt:52, on the re-entry path the inner recordChoice logs while the outer lock is still held. The re-entry test sets bootstrapEnabledForTest so that path isn't covered.
  • Nit: DispatcherFailureLogTests.kt:91, the throwing beforePoolChoice hook stays installed, so resetForTest runs one more failed choice before clearing hooks. Null it in finally, or clear hooks before choosing in resetForTest.
  • Nit: OneSignalDispatchers.kt:45, currentPools() went from private to internal, which widens raw pool access across the module.

…est hooks first, keep currentPools private

Co-authored-by: Cursor <cursoragent@cursor.com>
@abdulraqeeb33
abdulraqeeb33 merged commit 1dded18 into main Oct 22, 2026
6 of 10 checks passed
@abdulraqeeb33
abdulraqeeb33 deleted the ar/sdk-5581-readability branch October 22, 2026 18:43
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.

3 participants