Skip to content
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

Fix NPE for outcomeEventsController during initialization #1539

Merged
merged 2 commits into from
Mar 31, 2022

Conversation

nan-li
Copy link
Contributor

@nan-li nan-li commented Mar 3, 2022

Description

One Line Summary

Lazily initialize and access the OneSignal.outcomeEventsController, adding a null check when invoking its methods that wasn't checked before, as it may not have been initialized yet in the critical initialization step of setupContextListeners().

Details

Motivation

This is a speculative fix for a non-reproducible NullPointerException of outcomeEventsController.sendSavedOutcomes() during initialization, as reported by users in production here and here.

Context

During initialization, initWithContext() and setAppId() run. The first method also initializes the outcomeEventsController in the method setupContextListeners() see code.

After the appContext and appId are set by the above methods, the init() method runs, which calls OSOutcomeEventsController.sendSavedOutcomes() towards the method's end.

It may be that in some cases, the init() method is called before initWithContext() is fully complete, resulting in a null outcomeEventsController instance that init() relies on.

One GitHub reporter speculated it is due to adding setLanguage functionality in 4.4.0, and there are now more calls to initWithContext added in 4.4.1 for more FCM entry points.

It is possible that multiple threads are initializing and one hangs after setting the appContext but hasn't finished setupContextListeners.

This is ultimately speculative and it may be some other reason that outcomeEventsController is null.

Implementation Details

When we want to access OneSignal.outcomeEventsController, use lazy initialization and singleton pattern to access and create it via getOutcomeEventsController(). Use synchronization when creating the object.

There are some methods where outcomeEventsController is called such as in sendUniqueOutcome() where there is already a null check and the method returns if outcomeEventsController == null. In these methods I didn’t make any changes as it already had its own null check logic.

Scope

This affects OneSignal initialization.

Potential Problems

If the root issue is in fact that setupContextListeners() isn't completed before init() runs, it may be possible that languageContext isn't initialized either. In this case, while we move past the NPE for outcomeEventsController, we may encounter a NPE for languageContext next, such as in registerUserTask() or when it is passed as an argument to getInAppMessageController().

However, that may be far enough down the line that it won't pose a problem.

Testing

Unit testing

Added unit test to simulate setupContextListeners() not completing:

  • Add unit test initWithContext_setupContextListenersNotCompleted_doesNotProduceNPE to reproduce the NPE of outcomeEventsController in init().

  • Shadow the OneSignal.setupContextListeners() method to simulate it not completing (thus not initializing the outcomeEventsController).

  • However, the languageContext initialization is needed or else it leads to a subsequent NPE for this property in registerUserTask().

  • Change access of languageContext in OneSignal.java from private to package-private so it can be accessed in unit tests.

  • All other unit tests pass

Manual testing

App built with Android Studio 2020.3.1 with the OneSignal example app on a Pixel 5 with API 30.

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing
  • Any Public API changes are explained in the PR details and conform to existing APIs

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, but is insufficient as this error is not reproducible.

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

This change is Reviewable

- Use lazy initialization and singleton pattern to access and create outcomeEventsController
- When we call outcomeEventsController's methods, check if it is null, and if so, initialize it
- Follow https://www.infoworld.com/article/2077568/java-tip-67--lazy-instantiation.html
@nan-li nan-li changed the title WiP: Fix NPE for outcomeEventsController during initialization Fix NPE for outcomeEventsController during initialization Mar 21, 2022
@nan-li nan-li requested review from Jeasmine, jkasten2 and a team March 21, 2022 22:30
Copy link
Member

@jkasten2 jkasten2 left a comment

Choose a reason for hiding this comment

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

Logic changes look good, have a concern about the shadow changes though and it effecting other tests.

Reviewed 3 of 3 files at r1, all commit messages.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on @Jeasmine and @nan-li)


OneSignalSDK/unittest/src/test/java/com/onesignal/ShadowOneSignal.java, line 30 at r1 (raw file):

    */
   @Implementation
   public static void setupContextListeners(boolean wasAppContextNull) {

There are a number of tests using this shadow, this method changes the behavior quite a bit. Can we confirm if this change is needed at all?

If it is could we do one of the following?:

  1. By default call the original logic, we could add an if statement to check a flag and only set it in the test that needs this.
  2. Create a different class that shadows the OneSignal class and only use it in the test you need.

@nan-li nan-li requested a review from emawby March 23, 2022 18:14
- Add unit test: initWithContext_setupContextListenersNotCompleted_doesNotProduceNPE to reproduce the NPE of `outcomeEventsController` in `init()`.

- Shadow the `OneSignal.setupContextListeners()` method to simulate it not completing (thus not initializing the `outcomeEventsController`).

- Add new shadow of OneSignal besides ShadowOneSignal named `ShadowOneSignalWithMockSetupContextListeners` so other unit tests using `ShadowOneSignal` don't have behavior changed

- However, the `languageContext` initialization is needed or else it leads to a subsequent NPE for this property in `registerUserTask()`.

- Change access of `languageContext` in OneSignal.java from private to package-private so it can be accessed in unit tests.
@nan-li nan-li force-pushed the fix/outcomeEventsController_init_npe branch from 2cfe353 to fd9c957 Compare March 23, 2022 18:31
@nan-li
Copy link
Contributor Author

nan-li commented Mar 23, 2022

There are a number of tests using this shadow, this method changes the behavior quite a bit. Can we confirm if this change is needed at all?

If it is could we do one of the following?:

  1. By default call the original logic, we could add an if statement to check a flag and only set it in the test that needs this.
  2. Create a different class that shadows the OneSignal class and only use it in the test you need.

Good catch, I've created a new shadow class ShadowOneSignalWithMockSetupContextListeners just for the added unit test that has the modified setupContextListeners() method.

@nan-li nan-li requested a review from jkasten2 March 23, 2022 18:51
Copy link
Member

@jkasten2 jkasten2 left a comment

Choose a reason for hiding this comment

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

Reviewed 3 of 3 files at r2, all commit messages.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @emawby and @Jeasmine)

Copy link
Member

@jkasten2 jkasten2 left a comment

Choose a reason for hiding this comment

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

:lgtm:

Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @emawby and @Jeasmine)

@jkasten2
Copy link
Member

Tests failed but they are most likely flaky unrelated ones:
Test 1:

om.test.onesignal.GenerateNotificationRunner > shouldSetExpireTimeCorrectlyWhenMissingFromPayload FAILED
    java.lang.Exception: Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(getMainLooper()).idle() call.
        at org.robolectric.android.internal.AndroidTestEnvironment.checkStateAfterTestFailure(AndroidTestEnvironment.java:502)
        at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:581)
        at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:263)
        at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base/java.lang.Thread.run(Thread.java:834)
        Caused by:
        junit.framework.AssertionFailedError: expected:<1648320007> but was:<1648320006>
            at junit.framework.Assert.fail(Assert.java:57)
            at junit.framework.Assert.failNotEquals(Assert.java:329)
            at junit.framework.Assert.assertEquals(Assert.java:78)
            at junit.framework.Assert.assertEquals(Assert.java:159)
            at junit.framework.Assert.assertEquals(Assert.java:166)
            at com.test.onesignal.GenerateNotificationRunner.shouldSetExpireTimeCorrectlyWhenMissingFromPayload(GenerateNotificationRunner.java:1278)

Test 2:

com.test.onesignal.NotificationOpenedActivityHMSIntegrationTestsRunner > osIAMPreview_showsPreview FAILED
    java.lang.Exception: Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(getMainLooper()).idle() call.
        at org.robolectric.android.internal.AndroidTestEnvironment.checkStateAfterTestFailure(AndroidTestEnvironment.java:502)
        at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:581)
        at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:263)
        at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base/java.lang.Thread.run(Thread.java:834)
        Caused by:
        junit.framework.ComparisonFailure: expected:<PGh0bWw+PC9odG1sPgoKPHNjcmlwdD4KICAgIHNldFBsYXllclRhZ3MobnVsbCk7Cjwvc2NyaXB0Pg==> but was:<null>
            at junit.framework.Assert.assertEquals(Assert.java:85)
            at junit.framework.Assert.assertEquals(Assert.java:91)
            at com.test.onesignal.NotificationOpenedActivityHMSIntegrationTestsRunner.osIAMPreview_showsPreview(NotificationOpenedActivityHMSIntegrationTestsRunner.java:203)

Rerunning the tests to confirm.

@nan-li
Copy link
Contributor Author

nan-li commented Mar 23, 2022

shouldSetExpireTimeCorrectlyWhenMissingFromPayload has been regularly failing here, off by 1 second. I've re-ran the build a few times in the past 1-2 days.

Running the unit tests locally twice, they all pass.

@nan-li nan-li merged commit ad3e83a into main Mar 31, 2022
@nan-li nan-li deleted the fix/outcomeEventsController_init_npe branch March 31, 2022 17:42
nan-li added a commit that referenced this pull request Apr 8, 2022
@nan-li nan-li mentioned this pull request Apr 8, 2022
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.

2 participants