CustomerInfoManager: fixed deadlock caused by reading CustomerInfo inside of observer#2412
Conversation
…` 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`.
fb8e171 to
6099ab2
Compare
| // See https://github.com/RevenueCat/purchases-ios/issues/2410 | ||
| func testDelegateWithGetCustomerInfoCallDoesNotDeadlock() throws { | ||
| final class GetCustomerInfoPurchasesDelegate: NSObject, PurchasesDelegate { | ||
| func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) { |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Yeah that's part of another test but it doesn't hurt.
| // 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. |
There was a problem hiding this comment.
already asked through slack, just wondering why we need a read lock for customerInfo in the first place
There was a problem hiding this comment.
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 👍🏻
There was a problem hiding this comment.
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.
…` 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`.
… 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: 
… 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: 
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 #2410 and RevenueCat/react-native-purchases#579.
This adds 2 previously failing tests that reproduce this issue: one directly on
CustomerInfoManagerand another one utilizingPurchasesDelegate.These illustrate the issue: if the observer (through
CustomerInfoManager.monitorChangesorPurchasesDelegate) called a method that ended up readingCustomerInfoManager'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.