Skip to content

DeviceCache: changed NotificationCenter observation to be received on posting thread#2078

Merged
NachoSoto merged 2 commits into
mainfrom
device-cache-synchronous-user-defaults-observation
Nov 22, 2022
Merged

DeviceCache: changed NotificationCenter observation to be received on posting thread#2078
NachoSoto merged 2 commits into
mainfrom
device-cache-synchronous-user-defaults-observation

Conversation

@NachoSoto

@NachoSoto NachoSoto commented Nov 22, 2022

Copy link
Copy Markdown
Contributor

This could solve CSDK-519, and at the very least it makes the semantics simpler to understand.

The traditional NotificationCenter.addObserver seems to always lead to a jump to the main thread when modifying UserDefaults from any other thread.
See:

Thread 1:
0 libsystem_kernel.dylib 0x1fa81041c __psynch_cvwait + 8
1 libsystem_pthread.dylib 0x20aa5406c _pthread_cond_wait + 1232
2 Foundation 0x1b85b3800 -[NSOperation waitUntilFinished] + 508
3 CoreFoundation 0x1be16fe0c _CFXNotificationPost + 772
4 Foundation 0x1b85d04dc -[NSNotificationCenter postNotificationName:object:userInfo:] + 92
5 AudioMemos2 0x102ed18a4 specialized closure #1 in DeviceCache.cache(customerInfo:appUserID:) + 972964 (<compiler-generated>:0)

There were no potential race condition (and as far as we can tell, no deadlocks possible) because of how synchronization is done, but using the modern NotificationCenter.addObserver simplifies this, because handleUserDefaultsChanged will now be invoked synchronously in the calling thread.

@NachoSoto NachoSoto requested review from a team and aboedo November 22, 2022 21:21
@NachoSoto NachoSoto added the perf label Nov 22, 2022
…d on posting thread

This could solve [CSDK-519], and at the very least it makes the semantics simpler to understand.

The traditional `NotificationCenter.addObserver` seems to always lead to a jump to the main thread when modifying `UserDefaults` from any other thread.
See:
```
Thread 1:
0 libsystem_kernel.dylib 0x1fa81041c __psynch_cvwait + 8
1 libsystem_pthread.dylib 0x20aa5406c _pthread_cond_wait + 1232
2 Foundation 0x1b85b3800 -[NSOperation waitUntilFinished] + 508
3 CoreFoundation 0x1be16fe0c _CFXNotificationPost + 772
4 Foundation 0x1b85d04dc -[NSNotificationCenter postNotificationName:object:userInfo:] + 92
5 AudioMemos2 0x102ed18a4 specialized closure #1 in DeviceCache.cache(customerInfo:appUserID:) + 972964 (<compiler-generated>:0)
```

There were no potential race condition (and as far as we can tell, no deadlocks possible) because of how synchronization is done, but using the modern `NotificationCenter.addObserver` simplifies this, because `handleUserDefaultsChanged` will now be invoked synchronously in the calling thread.
@NachoSoto NachoSoto force-pushed the device-cache-synchronous-user-defaults-observation branch from 810476d to 879c4ca Compare November 22, 2022 21:30
@NachoSoto NachoSoto enabled auto-merge (squash) November 22, 2022 21:39
@NachoSoto NachoSoto merged commit 6474066 into main Nov 22, 2022
@NachoSoto NachoSoto deleted the device-cache-synchronous-user-defaults-observation branch November 22, 2022 21:39
NachoSoto added a commit that referenced this pull request Nov 23, 2022
**This is an automatic release.**

### Bugfixes
* Changed default `UserDefaults` from `.standard` to our own Suite
(#2046) via NachoSoto (@NachoSoto)
### Other Changes
* `Logging`: added log when configuring SDK in observer mode (#2065) via
NachoSoto (@NachoSoto)
* `PurchaseTesterSwiftUI`: added observer mode setting (#2052) via
NachoSoto (@NachoSoto)
* Exposed `SystemInfo.observerMode` to simplify code (#2064) via
NachoSoto (@NachoSoto)
* `Result.init(catching:)` with `async` method (#2060) via NachoSoto
(@NachoSoto)
* Updated schemes and project for Xcode 14.1 (#2081) via NachoSoto
(@NachoSoto)
* `PurchasesSubscriberAttributesTests`: simplified tests (#2056) via
NachoSoto (@NachoSoto)
* `DeviceCache`: removed `fatalError` for users not overriding
`UserDefaults` (#2079) via NachoSoto (@NachoSoto)
* `DeviceCache`: changed `NotificationCenter` observation to be received
on posting thread (#2078) via NachoSoto (@NachoSoto)
* `StoreKit1Wrapper`: added instance address when detecting transactions
(#2055) via NachoSoto (@NachoSoto)
* Fixed lint issues with `SwiftLint 0.5.0` (#2076) via NachoSoto
(@NachoSoto)
* `NSData+RCExtensionsTests`: improved errors (#2043) via NachoSoto
(@NachoSoto)
* `APITester`: fixed warning in `SubscriptionPeriodAPI` (#2054) via
NachoSoto (@NachoSoto)
* `Integration Tests`: always run them in random order locally (#2068)
via NachoSoto (@NachoSoto)

Co-authored-by: NachoSoto <ignaciosoto90@gmail.com>
NachoSoto added a commit that referenced this pull request Mar 24, 2023
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.
NachoSoto added a commit that referenced this pull request Mar 24, 2023
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.
NachoSoto added a commit that referenced this pull request 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.


[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 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
@vegaro vegaro added pr:other and removed pr:perf labels Sep 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants