Skip to content

CustomerInfoManager: fixed deadlock caused by reading CustomerInfo inside of observer#2412

Merged
NachoSoto merged 1 commit into
mainfrom
customer-info-manager-deadlock
Apr 19, 2023
Merged

CustomerInfoManager: fixed deadlock caused by reading CustomerInfo inside of observer#2412
NachoSoto merged 1 commit into
mainfrom
customer-info-manager-deadlock

Conversation

@NachoSoto

@NachoSoto NachoSoto commented Apr 17, 2023

Copy link
Copy Markdown
Contributor

Fixes #2410 and RevenueCat/react-native-purchases#579.

This adds 2 previously failing tests that reproduce this issue: one directly on CustomerInfoManager and another one utilizing PurchasesDelegate.
These illustrate the issue: if the observer (through CustomerInfoManager.monitorChanges or PurchasesDelegate) called a method that ended up reading CustomerInfoManager's data inside of the observation, the SDK would deadlock.

This is because we were invoking the observers while inside the lock. This was unnecessary, so this fixes the issue by ensuring that the observers receive updates after the lock is released, using a new OperationDispatcher.dispatchAsyncOnMainThread.

…` inside of observer

Fixes #2410 and RevenueCat/react-native-purchases#579

This adds 2 previously failing tests that reproduce this issue: one directly on `CustomerInfoManager` and another one utilizing `PurchasesDelegate`.
These illustrate the issue: if the observer (through `CustomerInfoManager.monitorChanges` or `PurchasesDelegate`) called a method that ended up reading `CustomerInfoManager`'s data inside of the observation, the SDK would deadlock.

This is because we were invoking the observers while inside the lock. This was unnecessary, so this fixes the issue by ensuring that the observers receive updates after the lock is released, using a new `OperationDispatcher.dispatchAsyncOnMainThread`.
@NachoSoto NachoSoto force-pushed the customer-info-manager-deadlock branch from fb8e171 to 6099ab2 Compare April 17, 2023 23:21

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

// See https://github.com/RevenueCat/purchases-ios/issues/2410
func testDelegateWithGetCustomerInfoCallDoesNotDeadlock() throws {
final class GetCustomerInfoPurchasesDelegate: NSObject, PurchasesDelegate {
func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we add a check to make sure this is actually called? To make sure nothing changes that could cause us to miss the important part of the test (this might be part of another test so it might be ok).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah that's part of another test but it doesn't hurt.

Comment on lines +253 to +254
// This must be async to prevent deadlocks if the observer calls a method that ends up reading
// this class' data. By making it async, the closure is invoked outside of the lock.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

already asked through slack, just wondering why we need a read lock for customerInfo in the first place

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm yeah I think that's leftover from the prior implementation. This whole method could be a lot simpler if we just read the underlying data (info, observers, etc) at the beginning and we do the rest outside of the lock, then we wouldn't need the dispatch async change. I'll do that and it should still make the tests pass 👍🏻

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh wait we do modify the lastSentCustomerInfo that's why it needs to be thread safe so we don't end up reading and writing to it outside the lock.

@NachoSoto NachoSoto merged commit 8201edf into main Apr 19, 2023
@NachoSoto NachoSoto deleted the customer-info-manager-deadlock branch April 19, 2023 18:45
NachoSoto added a commit that referenced this pull request Apr 19, 2023
…` inside of observer (#2412)

Fixes #2410 and
RevenueCat/react-native-purchases#579.

This adds 2 previously failing tests that reproduce this issue: one
directly on `CustomerInfoManager` and another one utilizing
`PurchasesDelegate`.
These illustrate the issue: if the observer (through
`CustomerInfoManager.monitorChanges` or `PurchasesDelegate`) called a
method that ended up reading `CustomerInfoManager`'s data inside of the
observation, the SDK would deadlock.

This is because we were invoking the observers while inside the lock.
This was unnecessary, so this fixes the issue by ensuring that the
observers receive updates after the lock is released, using a new
`OperationDispatcher.dispatchAsyncOnMainThread`.
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.

Purchases.shared.purchase completion not called when calling getCustomerInfo from receivedUpdated in PurchasesDelegate

4 participants