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

DeviceCache: workaround for potential deadlock #2375

Merged
merged 3 commits into from
Mar 24, 2023
Merged

Conversation

NachoSoto
Copy link
Contributor

@NachoSoto NachoSoto commented Mar 24, 2023

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 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.

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
Copy link

codecov bot commented Mar 24, 2023

Codecov Report

Merging #2375 (9ace4df) into main (339e13d) will decrease coverage by 0.05%.
The diff coverage is 100.00%.

@@            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     
Impacted Files Coverage Δ
Sources/Caching/DeviceCache.swift 94.76% <100.00%> (-0.05%) ⬇️
Sources/Identity/IdentityManager.swift 99.20% <100.00%> (+0.01%) ⬆️

... 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
Copy link
Contributor Author

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.

@NachoSoto NachoSoto merged commit 3246a58 into main Mar 24, 2023
@NachoSoto NachoSoto deleted the device-cache-deadlock branch March 24, 2023 23:15
NachoSoto added a commit that referenced this pull request Apr 10, 2023
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
NachoSoto added a commit that referenced this pull request May 12, 2023
… not blocked

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.
NachoSoto added a commit that referenced this pull request May 12, 2023
… 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)
NachoSoto added a commit that referenced this pull request May 25, 2023
… 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)
NachoSoto added a commit that referenced this pull request Jul 14, 2023
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr:fix A bug fix
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Revenuecat hangs the UI and the app crashes because of memory
2 participants