-
Notifications
You must be signed in to change notification settings - Fork 316
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
DeviceCache
: workaround for potential deadlock
#2375
Conversation
Fixes #2252, #2364, RevenueCat/react-native-purchases#579, [SDKONCALL-214], [SDKONCALL-238], [SDKONCALL-241]. See also #2078 and #2079. ### Background #2078 solved a potential deadlock in `DeviceCache`. This could happen when a background thread was in the middle of writing to `UserDefaults` through `DeviceCache`, and at the same time, the main thread wanted to read from it. This is normally fine, except that if there are any `NotificationCenter` observations on the `UserDefaults` used, the background thread would then post that notification and wait for the main thread, leading to a deadlock. What #2078 did was to use a different method to make sure that the observation was received in the calling thread. Additionally, #2079 avoided that observation whenever we used a different `UserDefaults`. ### Problem When the SDK changed to a custom `UserDefaults` (#2046), we made the choice to only do this if there was no prior data. However, if an app has prior RevenueCat data saved in `UserDefaults.standard`, that instance is used. This potential deadlock is possible for any app with prior data, that also contains observations to `UserDefaults`, either manually through `NotificationCenter`, or using something like `@AppStorage` with `SwiftUI`. ### Solution `DeviceCache.cachedAppUserID` is a very commonly used method. This PR changes it to an in-memory cache, initialized to the `UserDefaults` value. By doing that, most code paths that rely on this value won't risk deadlocking with `UserDefaults` writes that jump threads. ### Long term I've filed SDK-3034 to fully migrate data from `UserDefaults.standard` to avoid this problem altogether in the future.
Codecov Report
@@ Coverage Diff @@
## main #2375 +/- ##
==========================================
- Coverage 86.78% 86.73% -0.05%
==========================================
Files 198 198
Lines 13206 13204 -2
==========================================
- Hits 11461 11453 -8
- Misses 1745 1751 +6
... and 2 files with indirect coverage changes Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
let currentAppUserIDLooksAnonymous = Self.userIsAnonymous(self.currentAppUserID) | ||
let isLegacyAnonymousAppUserID = self.currentAppUserID == self.deviceCache.cachedLegacyAppUserID | ||
lazy var currentAppUserIDLooksAnonymous = Self.userIsAnonymous(self.currentAppUserID) | ||
lazy var isLegacyAnonymousAppUserID = self.currentAppUserID == self.deviceCache.cachedLegacyAppUserID |
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 makes it so that isLegacyAnonymousAppUserID
might not have to be read if the first one is already true
.
5b2c479
to
2d14f97
Compare
Fixes #2252, #2364, [SDKONCALL-214], [SDKONCALL-241]. See also #2078 and #2079. when a background thread was in the middle of writing to `UserDefaults` through `DeviceCache`, and at the same time, the main thread wanted to read from it. This is normally fine, except that if there are any `NotificationCenter` observations on the `UserDefaults` used, the background thread would then post that notification and wait for the main thread, leading to a deadlock. What #2078 did was to use a different method to make sure that the observation was received in the calling thread. Additionally, #2079 avoided that observation whenever we used a different `UserDefaults`. When the SDK changed to a custom `UserDefaults` (#2046), we made the choice to only do this if there was no prior data. However, if an app has prior RevenueCat data saved in `UserDefaults.standard`, that instance is used. This potential deadlock is possible for any app with prior data, that also contains observations to `UserDefaults`, either manually through `NotificationCenter`, or using something like `@AppStorage` with `SwiftUI`. `DeviceCache.cachedAppUserID` is a very commonly used method. This PR changes it to an in-memory cache, initialized to the `UserDefaults` value. By doing that, most code paths that rely on this value won't risk deadlocking with `UserDefaults` writes that jump threads. I've filed SDK-3034 to fully migrate data from `UserDefaults.standard` to avoid this problem altogether in the future. [SDKONCALL-214]: https://revenuecats.atlassian.net/browse/SDKONCALL-214?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [SDKONCALL-238]: https://revenuecats.atlassian.net/browse/SDKONCALL-238?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [SDKONCALL-241]: https://revenuecats.atlassian.net/browse/SDKONCALL-241?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
… not blocked (#2463) We've had a few reports of deadlocks (#2412, #2375). This might have not detected them, but it would detect future issues, as well as busy operations running on the main thread. Example: ![Screenshot 2023-05-01 at 9 50 22 AM](https://user-images.githubusercontent.com/685609/235492076-b84195ca-67e7-4762-8c56-49b1758c8534.png)
… not blocked (#2463) We've had a few reports of deadlocks (#2412, #2375). This might have not detected them, but it would detect future issues, as well as busy operations running on the main thread. Example: ![Screenshot 2023-05-01 at 9 50 22 AM](https://user-images.githubusercontent.com/685609/235492076-b84195ca-67e7-4762-8c56-49b1758c8534.png)
This was introduced in #2463, as a way to verify the SDK didn't have any deadlocks (#2412, #2375). However, it has caused more trouble than it's worth, because that loop keeps the main thread busy. This solution proposed by @aboedo makes it only check every 3 seconds. If there is a deadlock, it would still detect it. But after it's verified there is no deadlock, it won't check again until that interval has elapsed again. This makes it so that on a test that takes 6 seconds to run, we only execute this code 2 times instead of... a lot.
Fixes #2252, #2364, SDKONCALL-214, SDKONCALL-241.
See also #2078 and #2079.
Background
#2078 solved a potential deadlock in
DeviceCache
. This could happen when a background thread was in the middle of writing toUserDefaults
throughDeviceCache
, and at the same time, the main thread wanted to read from it.This is normally fine, except that if there are any
NotificationCenter
observations on theUserDefaults
used, the background thread would then post that notification and wait for the main thread, leading to a deadlock. What #2078 did was to use a different method to make sure that the observation was received in the calling thread.Additionally, #2079 avoided that observation whenever we used a different
UserDefaults
.Problem
When the SDK changed to a custom
UserDefaults
(#2046), we made the choice to only do this if there was no prior data.However, if an app has prior RevenueCat data saved in
UserDefaults.standard
, that instance is used.This potential deadlock is possible for any app with prior data, that also contains observations to
UserDefaults
, either manually throughNotificationCenter
, or using something like@AppStorage
withSwiftUI
.Solution
DeviceCache.cachedAppUserID
is a very commonly used method. This PR changes it to an in-memory cache, initialized to theUserDefaults
value.By doing that, most code paths that rely on this value won't risk deadlocking with
UserDefaults
writes that jump threads.Long term
I've filed SDK-3034 to fully migrate data from
UserDefaults.standard
to avoid this problem altogether in the future.