Refactor: move part of initWithContext off main thread#2368
Refactor: move part of initWithContext off main thread#2368jinliu9508 merged 1 commit into5.4-mainfrom
Conversation
57aed46 to
f048835
Compare
973461e to
38f9839
Compare
abdulraqeeb33
left a comment
There was a problem hiding this comment.
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.
can we create variables for these version numbers? atleast until we move to kotlin dsl
There was a problem hiding this comment.
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.
maybe we dont need these two methods?
There was a problem hiding this comment.
Removed in the follow up commit
d8a1a50 to
97932bf
Compare
8e106eb to
c80e0fb
Compare
abdulraqeeb33
left a comment
There was a problem hiding this comment.
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.
did we need this here @jinliu9508 ? looks like jobFinished was not called earlier when returning false
There was a problem hiding this comment.
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.
typo - upgradeReceiver
There was a problem hiding this comment.
Addressed in the fixup
jkasten2
left a comment
There was a problem hiding this comment.
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.
| * A generic latch that allows waiting for asynchronous initialization or completion | ||
| * with timeout support and detailed logging. |
There was a problem hiding this comment.
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.
| try { | ||
| AndroidUtils.isRunningOnMainThread() | ||
| } catch (_: Throwable) { | ||
| false | ||
| } |
There was a problem hiding this comment.
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.
This doesn't need to run on the main thread, better if it doesn't
There was a problem hiding this comment.
Addressed in the fixup
| if (isOrderedBroadcast) { | ||
| // Prevents other BroadcastReceivers from firing | ||
| abortBroadcast() | ||
| withContext(Dispatchers.Main) { |
There was a problem hiding this comment.
This doesn't need to run on the main thread, better if it doesn't
There was a problem hiding this comment.
Addressed in the fixup
| // run init in background | ||
| suspendifyOnThread { |
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
addressed in the followup
| context: Context, | ||
| appId: String?, | ||
| ): Boolean = | ||
| withContext(Dispatchers.Default) { |
There was a problem hiding this comment.
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.
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.
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.
addressed in the followup; no longer in try-catch
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.
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.
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.
awaitOrThrow no longer exists, remove this in the comment
There was a problem hiding this comment.
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.
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.
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