-
Notifications
You must be signed in to change notification settings - Fork 376
Refactor: move part of initWithContext off main thread #2368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
57aed46 to
f048835
Compare
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt
Outdated
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt
Outdated
Show resolved
Hide resolved
Examples/OneSignalDemo/app/src/test/java/com.onesignal.sdktest/application/DemoAppTests.kt
Outdated
Show resolved
Hide resolved
973461e to
38f9839
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a few nits and some must haves
| testImplementation("org.robolectric:robolectric:4.8.1") | ||
| // kotest-extensions-android allows Robolectric to work with Kotest via @RobolectricTest | ||
| testImplementation("br.com.colman:kotest-extensions-android:0.1.1") | ||
| testImplementation("androidx.test:core-ktx:1.4.0") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we create variables for these version numbers? atleast until we move to kotlin dsl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These testImplementations are no longer there as we removed the app level tests out.
| /** | ||
| * Create an awaiter for OneSignal SDK specifically. | ||
| */ | ||
| fun createOneSignalAwaiter() = LatchAwaiter("OneSignal SDK") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we dont need these two methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed in the follow up commit
...gnalSDK/onesignal/core/src/test/java/com/onesignal/core/internal/application/SDKInitTests.kt
Outdated
Show resolved
Hide resolved
...tions/src/main/java/com/onesignal/notifications/activities/NotificationOpenedActivityBase.kt
Outdated
Show resolved
Hide resolved
...l/notifications/src/main/java/com/onesignal/notifications/bridges/OneSignalHmsEventBridge.kt
Outdated
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt
Outdated
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/LatchAwaiter.kt
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt
Outdated
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt
Outdated
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt
Outdated
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/ThreadUtils.kt
Outdated
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt
Outdated
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt
Outdated
Show resolved
Hide resolved
d8a1a50 to
97932bf
Compare
8e106eb to
c80e0fb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small nits but otherwise looks good
| suspendifyOnThread { | ||
| // init OneSignal in background | ||
| if (!OneSignal.initWithContext(this)) { | ||
| jobFinished(jobParameters, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did we need this here @jinliu9508 ? looks like jobFinished was not called earlier when returning false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, since now we always return true, jobFinished needs to be called to cancel the job if initialization is unsuccessful.
| // init OneSignal and enqueue restore work in background | ||
| suspendifyOnThread { | ||
| if (!OneSignal.initWithContext(context.applicationContext)) { | ||
| Logging.warn("NotificationRestoreReceiver skipped due to failed OneSignal init") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo - upgradeReceiver
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in the fixup
...gnalSDK/onesignal/notifications/src/main/java/com/onesignal/NotificationOpenedActivityHMS.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR is based on main, however we discussed we want to release it with JWT so it should be based on top of that.
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt
Outdated
Show resolved
Hide resolved
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignal.kt
Outdated
Show resolved
Hide resolved
| * A generic latch that allows waiting for asynchronous initialization or completion | ||
| * with timeout support and detailed logging. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove the word "generic" and explain in more detail the purpose of this class? Another sentence or two worth of specifics around the main / UI thread and ANR use-cases.
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/threading/LatchAwaiter.kt
Show resolved
Hide resolved
| try { | ||
| AndroidUtils.isRunningOnMainThread() | ||
| } catch (_: Throwable) { | ||
| false | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove the try-catch here? I don't see how this could ever throw. Also pre-existing usages in the code base also don't have a try-catch.
| if (isOrderedBroadcast) { | ||
| resultCode = Activity.RESULT_OK | ||
| private suspend fun setSuccessfulResultCode() { | ||
| withContext(Dispatchers.Main) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to run on the main thread, better if it doesn't
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in the fixup
| if (isOrderedBroadcast) { | ||
| // Prevents other BroadcastReceivers from firing | ||
| abortBroadcast() | ||
| withContext(Dispatchers.Main) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to run on the main thread, better if it doesn't
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in the fixup
...fications/src/main/java/com/onesignal/notifications/receivers/NotificationDismissReceiver.kt
Show resolved
Hide resolved
...esignal/notifications/src/main/java/com/onesignal/notifications/receivers/UpgradeReceiver.kt
Show resolved
Hide resolved
| // run init in background | ||
| suspendifyOnThread { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have concerns this is going to have side-effects. Mainly that notificationPayloadProcessorHMS.handleHMSNotificationOpenIntent will now run AFTER finish(). I recommend use the same logic from NotificationOpenedActivityBase to avoid changing order of execution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, the logic now runs in a suspendifyBlocking. This might cause ANR as we can no longer move it to the background.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic is now unchanged. However, ANR may still occur if initialization took too long
| withTimeout(LatchAwaiter.ANDROID_ANR_TIMEOUT_MS) { | ||
| runBlocking { | ||
| suspendInitInternal(context, null) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
withTimeout will cause suspendInitInternal to stop executing. This could lead the SDK to being in an unpredictable state so I would rather we let it run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed in the followup
| } catch (e: TimeoutCancellationException) { | ||
| Logging.log(LogLevel.ERROR, "initWithContext: Initialization timed out") | ||
| false | ||
| } catch (e: Exception) { | ||
| Logging.log(LogLevel.ERROR, "initWithContext: Initialization failed with exception", e) | ||
| false | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't catch here at all, it is going to hide root causes, as all we are going to get is a very generic throw IllegalStateException("Initialization failed. Cannot proceed.") later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed in the followup
| context: Context, | ||
| appId: String?, | ||
| ): Boolean = | ||
| withContext(Dispatchers.Default) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see two issues with this withContext line
- I don't think we should switch threads in this method, let the caller pick the thread / context to simply this.
- This could cause delays in init based on what else the app might be putting on this generic context. We also don't use
Dispatchers.Defaultanywhere else in the code base either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed in the followup; the method is now no long in a thread
| } catch (e: Throwable) { | ||
| Logging.error("suspendInitInternal failed!", e) | ||
| initState = InitState.FAILED | ||
| latchAwaiter.release() | ||
| return@withContext false | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching Throwable is going to hide root cause from crash reports, since we are not rethrowing here. If this was pre-existing before this PR it is ok to keep for now, but I don't think that was the case.
I do think the logic in the catch make sense to do if there is a failure, so let's keep doing that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed in the followup; no longer in try-catch
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt
Show resolved
Hide resolved
32acee4 to
695d830
Compare
| synchronized(initLock) { | ||
| if (initState.isSDKAccessible()) { | ||
| Logging.log(LogLevel.DEBUG, "initWithContext: SDK already initialized or in progress") | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| // bootstrap services | ||
| startupService.bootstrap() | ||
| initState = InitState.IN_PROGRESS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
synchronized(initLock) { } block needs to be around initState = InitState.IN_PROGRESS too, otherwise it doesn't solve anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in the fixup
| * Usage: | ||
| * val awaiter = LatchAwaiter("OneSignal SDK Init") | ||
| * awaiter.release() // when done | ||
| * awaiter.awaitOrThrow() // or await() to just check |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awaitOrThrow no longer exists, remove this in the comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in the fixup
| // do not do this again if already initialized or init is in progress | ||
| synchronized(initLock) { | ||
| if (initState.isSDKAccessible()) { | ||
| Logging.log(LogLevel.DEBUG, "initWithContext: SDK already initialized or in progress") | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| // bootstrap services | ||
| startupService.bootstrap() | ||
| initState = InitState.IN_PROGRESS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
synchronized(initLock) { } block has to be around initState = InitState.IN_PROGRESS, as only around this read doesn't do anything on it's own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in the fixup
23d672f to
808d1cc
Compare
Description
One Line Summary
Move slow/IO-bound parts of initWithContext off the main thread and add an async overload with a completion callback for internal usage.
Details
Motivation
Avoid main-thread stalls and potential ANRs during initialization, especially when SharedPreferences or other disk/network operations are involved. Provide an async path so app components (Activities/Receivers) can trigger initialization without blocking UI or risking Receiver timeouts.
Scope
Testing
Unit testing
Manual testing
Affected code checklist
Checklist
Overview
Testing
Final pass
This change is