Make ServiceProvider.getService thread safe#2012
Merged
Conversation
Create test to confirm that ServiceBuilder.getService() should return the same instance, even when accessed from two threads. To ensure the test is consistent we made the constructor slow so the thread both end on the same line of code.
Add synchronized around all access to ServiceProvider's serviceMap. This ensures nothing can modify this map, resulting in multiple instance of a class being return unexpectedly. Another critical path is the call to resolve(this), where this logic could also take enough time that could case threading issues that is now wrapped in this new lock. This most likely will fix the real world problem we have been seeing where IApplicationService.appContext is null unexpectedly. One discovered path is ReceiveReceiptWorker is created from the Android OS on a background thread and it calls getService() without first calling OneSignal.initWithContext, resulting in a race condition where two instance of IApplicationService can be created. ReceiveReceiptWorker probably should call OneSignal.initWithContext first, but changing this is out of scope for this commit.
No logic changes in this commit
emawby
approved these changes
Mar 1, 2024
Merged
18 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
One Line Summary
Make
ServiceProviderthread safe, to avoid cases where it can unexpectedly return different instances for the same interface.Details
Motivation
We are getting a number of crash reports with
appContextbeingnulland I believeServiceProvider.getServicenot being thread safe is cause. (but unknown if the only one)Scope
Only thread safely changes to
ServiceProvider.kt.SDK Internal Details
When investigating we found a case where two instances of
IApplicationServicecould be created. Specifically if an instance ofReceiveReceiptWorkeris created by the Android Operating System whenOneSignal.initWithContextis called (by the app developer or internally by the SDK). This can result in two thread callingServiceProvider.getService(), creating the two instances of a class unexpectedly.What This May Fix
While this fix addresses a single known cause of
OneSignal.initWithContextthrowing on anullContexterror, it is unclear if this is the only source of this, or by how much this fix will help lower these crashes.Reproducing the Crash in an app
While I could not reproduce the issue in a real world scenario there is a unit test in this PR ServiceProviderTest. Also a integration style test can be done by running the example app in the crash-with-null-context-on-initWithContext branch, see aa32927 through for more details.
Crashes
This PR may address NullPointerException from
initWithContextlike this one:#1990
Testing
Unit testing
Added a new test
Created a new ServiceProviderTest test confirm that
ServiceBuilder.getService()should return the same instance, even when accessed from two threads. To ensure the test is consistent we made the constructor slow so both threads end up on the same line of the production code we want to test.Manual testing
Tested on an Android 6 and Android 14 emulator, ensuring notifications display.
Affected code checklist
Checklist
Overview
Testing
Final pass
This change is