RevenueCat / purchases-ios

In-app purchases and subscriptions made easy. Support for iOS, watchOS, tvOS, macOS, and visionOS.
https://www.revenuecat.com/
MIT License
2.3k stars 308 forks source link

crash: EXC_BAD_ACCESS in 4.2.1, DeviceCache.swift - Line 123 #1527

Closed quantamrhino closed 2 years ago

quantamrhino commented 2 years ago

Describe the bug Crash in DeviceCache.cache(customerInfo:appUserID:) + 123 Crashed: Backend callbackQueue EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x0000000b2005a2e0

see stack trace below

  1. Environment
    1. Platform: iOS
    2. SDK version: 4.2.1
    3. StoreKit 2 enabled (Y/N): not sure.
    4. OS version: 15.5.0
    5. Xcode version: 13.3.1
    6. How widespread is the issue. Percentage of devices affected. unknown as of now
  2. Debug logs that reproduce the issue no logs available for this crash
  3. Steps to reproduce, with a description of expected vs. actual behavior No steps as such - happened randomly
  4. Other information (e.g. stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, etc.)
Crashed: Backend callbackQueue
EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x0000000b2005a2e0
0
libobjc.A.dylib
objc_release + 16
1
Foundation
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] + 676
2
Foundation
-[NSObject(NSKeyValueObservingPrivate) _notifyObserversOfChangeFromValuesForKeys:toValuesForKeys:] + 816
3
CoreFoundation
-[CFPrefsSource forEachObserver:] + 332
4
CoreFoundation
-[CFPrefsSource _notifyObserversOfChangeFromValuesForKeys:toValuesForKeys:] + 112
5
CoreFoundation
___CFPrefsDeliverPendingKVONotificationsGuts_block_invoke + 440
6
CoreFoundation
__CFDictionaryApplyFunction_block_invoke + 28
7
CoreFoundation
CFBasicHashApply + 148
8
CoreFoundation
CFDictionaryApplyFunction + 328
9
CoreFoundation
_CFPrefsDeliverPendingKVONotificationsGuts + 300
10
CoreFoundation
-[_CFXPreferences _deliverPendingKVONotifications] + 96
11
CoreFoundation
__108-[_CFXPreferences(SearchListAdditions) withSearchListForIdentifier:container:cloudConfigurationURL:perform:]_block_invoke + 428
12
CoreFoundation
normalizeQuintuplet + 356
13
CoreFoundation
-[_CFXPreferences withSearchListForIdentifier:container:cloudConfigurationURL:perform:] + 152
14
CoreFoundation
-[_CFXPreferences setValue:forKey:appIdentifier:container:configurationURL:] + 128
15
CoreFoundation
_CFPreferencesSetAppValueWithContainerAndConfiguration + 136
16
Foundation
-[NSUserDefaults(NSUserDefaults) setObject:forKey:] + 84
17
<my app>
DeviceCache.swift - Line 123
closure #1 in DeviceCache.cache(customerInfo:appUserID:) + 123 

Additional context Add any other context about the problem here.

purchase state is unsubscribed. was on mobile data and not on wifi. app was in foreground, and buttons were pressed, and randomly the crash happened. hasn't happened again today

taquitos commented 2 years ago

In this case, it looks like when RevenueCat calls

$0.set(customerInfo, forKey: CacheKeyBases.customerInfoAppUserDefaults + appUserID)

at DeviceCache.swift line 123, that is causing the UserDefaults object to trigger some KVO notification in Foundation. By the look of it, Foundation is the culprit here.

I see that you're using iOS 15.5, can you retest what you're doing on a non-beta?

There are a couple other scenarios that might trigger this, like DeviceCache being deallocated (we don't do that)- however, if you ~don't store the Purchases object somewhere for the life of your app~ call configure two times, it is possible that is happening. I doubt it, though, it's more likely a beta thing 😄

taquitos commented 2 years ago

Another question we have-

If you're passing a UserDefaults object to Purchases, are you doing any KVO / UserNotifications on the UserDefaults object you're passing?

quantamrhino commented 2 years ago

Thanks for your quick response! I don't have a spare device with a release build yet :-( Also I don't know how to repro this .. it's happened once, seemingly randomly. No userdefaults/kvo being passed to purchases. The configure() is called in App's init() once. I'll keep a lookout for this crash again.

ZeeWanderer commented 2 years ago

XCode 13.3.1 reports runtime concurrency error for DeviceCache.swift lines 123 and 390, specifically purchases-ios/Sources/Caching/DeviceCache.swift: runtime: SwiftUI: Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates..

NachoSoto commented 2 years ago

Where is that coming from? The SDK doesn't use SwiftUI 🤔

ZeeWanderer commented 2 years ago

@NachoSoto I think that is just context-based message, the project itself where RevenueCat is being used is written with SwiftUI and so the runtime is SwiftUI, therefore the message is SwiftUI flavored.

NachoSoto commented 2 years ago

AFAIK this message:

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates

Can only be produced by SwiftUI or maybe Combine, neither of which are used by the SDK. I could be wrong, but this sounds like the SDK calling a block outside the main thread and your app updating the UI (maybe an ObservableObject?). If you can show us more code where this is coming from in the app maybe we could also make sure that the SDK doesn't invoke callbacks outside the main thread to avoid this.

ZeeWanderer commented 2 years ago

I am using SDK in ObservableObject but without callbacks at all, i use provided async/await variants. According to trace in both cases (123, 390) there is an HTTP request for customer info.

trace for 123: ``` #0 0x0000000000000000 in AttributeInvalidatingSubscriber.invalidateAttribute() () #1 0x0000000000000000 in AttributeInvalidatingSubscriber.receive(_:) () #2 0x0000000000000000 in protocol witness for Subscriber.receive(_:) in conformance AttributeInvalidatingSubscriber<τ_0_0> () #3 0x0000000000000000 in SubscriptionLifetime.Connection.receive(_:) () #4 0x0000000000000000 in ObservableObjectPublisher.Inner.send() () #5 0x0000000000000000 in ObservableObjectPublisher.send() () #6 0x0000000000000000 in UserDefaultObserver.Target.send() () #7 0x0000000000000000 in UserDefaultObserver.noteDefaultChange() () #8 0x0000000000000000 in UserDefaultObserver.userDefaultsDidChange(_:) () #9 0x0000000000000000 in @objc UserDefaultObserver.userDefaultsDidChange(_:) () #10 0x0000000000000000 in __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ () #11 0x0000000000000000 in ___CFXRegistrationPost_block_invoke () #12 0x0000000000000000 in _CFXRegistrationPost () #13 0x0000000000000000 in _CFXNotificationPost () #14 0x0000000000000000 in -[NSNotificationCenter postNotificationName:object:userInfo:] () #15 0x0000000000000000 in closure #1 in DeviceCache.cache(customerInfo:appUserID:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Caching/DeviceCache.swift:123 #16 0x0000000000000000 in closure #1 in SynchronizedUserDefaults.write(_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Misc/SynchronizedUserDefaults.swift:35 #17 0x0000000000000000 in partial apply for closure #1 in SynchronizedUserDefaults.write(_:) () #18 0x0000000000000000 in closure #1 in Atomic.withValue<τ_0_0>(_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Misc/Atomic.swift:71 #19 0x0000000000000000 in partial apply for closure #1 in Atomic.withValue<τ_0_0>(_:) () #20 0x0000000000000000 in Lock.perform<τ_0_0>(_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Misc/Lock.swift:30 #21 0x0000000000000000 in Atomic.withValue<τ_0_0>(_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Misc/Atomic.swift:70 #22 0x0000000000000000 in SynchronizedUserDefaults.write(_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Misc/SynchronizedUserDefaults.swift:34 #23 0x0000000000000000 in DeviceCache.cache(customerInfo:appUserID:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Caching/DeviceCache.swift:122 #24 0x0000000000000000 in CustomerInfoManager.cache(customerInfo:appUserID:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Identity/CustomerInfoManager.swift:155 #25 0x0000000000000000 in closure #1 in closure #1 in CustomerInfoManager.fetchAndCacheCustomerInfo(appUserID:isAppBackgrounded:completion:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Identity/CustomerInfoManager.swift:47 #26 0x0000000000000000 in CustomerInfoResponseHandler.handle(customerInfoResponse:completion:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/Operations/Handling/CustomerInfoResponseHandler.swift:32 #27 0x0000000000000000 in closure #1 in closure #2 in GetCustomerInfoOperation.getCustomerInfo(completion:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/Operations/GetCustomerInfoOperation.swift:68 #28 0x0000000000000000 in Sequence.forEach(_:) () #29 0x0000000000000000 in closure #1 in CallbackCache.performOnAllItemsAndRemoveFromCache(withCacheable:_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/Caching/CallbackCache.swift:52 #30 0x0000000000000000 in thunk for @callee_guaranteed () -> () () #31 0x0000000000000000 in thunk for @escaping @callee_guaranteed () -> () () #32 0x0000000000000000 in _dispatch_client_callout () #33 0x0000000000000000 in _dispatch_lane_barrier_sync_invoke_and_complete () #34 0x0000000000000000 in CallbackCache.performOnAllItemsAndRemoveFromCache(withCacheable:_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/Caching/CallbackCache.swift:47 #35 0x0000000000000000 in closure #2 in GetCustomerInfoOperation.getCustomerInfo(completion:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/Operations/GetCustomerInfoOperation.swift:67 #36 0x0000000000000000 in closure #1 in HTTPClient.Request.init<τ_0_0>(httpRequest:headers:completionHandler:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/HTTPClient.swift:93 #37 0x0000000000000000 in HTTPClient.handle(urlResponse:request:urlRequest:data:error:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/HTTPClient.swift:237 #38 0x0000000000000000 in closure #1 in HTTPClient.start(request:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/HTTPClient.swift:281 #39 0x0000000000000000 in thunk for @escaping @callee_guaranteed (@guaranteed Data?, @guaranteed NSURLResponse?, @guaranteed Error?) -> () () #40 0x0000000000000000 in ___lldb_unnamed_symbol526$$CFNetwork () #41 0x0000000000000000 in ___lldb_unnamed_symbol746$$CFNetwork () #42 0x0000000000000000 in _dispatch_call_block_and_release () #43 0x0000000000000000 in _dispatch_client_callout () #44 0x0000000000000000 in _dispatch_lane_serial_drain () #45 0x0000000000000000 in _dispatch_lane_invoke () #46 0x0000000000000000 in _dispatch_workloop_worker_thread () #47 0x0000000000000000 in _pthread_wqthread () #48 0x0000000000000000 in start_wqthread () ```
NachoSoto commented 2 years ago

Can you show us the code for that? You'll need to make sure your ObservableObject is updated in the MainActor.

ZeeWanderer commented 2 years ago

Code attached. But as far as i managed to investigate the problems is that DeviceCache.cache(customerInfo:appUserID:) is executed on Backend callbackQueue

Code ```swift final class Store: ObservableObject { @globalActor actor StoreActor { static let shared = StoreActor() @inlinable static func compareExchange(_ value: inout Value, expected: Value, desired: Value) -> (Bool, Value) where Value : Equatable { if value == expected { let original = value value = desired return (true, original) } else { return (false, value) } } @inlinable static func run(resultType: T.Type = T.self, body: @StoreActor @Sendable () throws -> T) async rethrows -> T where T : Sendable { return try await body() } } private enum ProductRefreshState: Int { case none = 1, inProgress = 2, success = 4 } // MARK: Published @Published private(set) var isSubscribed: Bool? = nil @Published private(set) var accountItems: [Package] @Published private(set) var consumables: [Package] @Published private(set) var subscriptions: [Package] @Published private(set) var storeError : Error? = nil private var productRefreshStatus = ProductRefreshState.none private static let subscriptionTier: [LogicaStoreProduct: SubscriptionTier] = [ .subscription_standard: .standard ] // MARK: init init() { //Initialize empty products then do a product request asynchronously to fill them in. accountItems = [] consumables = [] subscriptions = [] Task { await startup() await listen_for_updates() } Task { [unowned self] in //Initialize the store by starting a product request. await requestProductsIfNeeded() } } } // MARK: - Internals extension Store { // MARK: internal_check_trial(productIdentifiers:) @MainActor private static func internal_check_trial(productIdentifiers: [String]) async -> [String : IntroEligibility] { debug_print("Logica::Store::internal_check_trial(productIdentifiers: ): thread: \(Thread.current)") return await Purchases.shared.checkTrialOrIntroDiscountEligibility(productIdentifiers: productIdentifiers) } // MARK: internal_check_trial(product:) @MainActor private static func internal_check_trial(product: StoreProduct) async -> IntroEligibilityStatus { debug_print("Logica::Store::internal_check_trial(product:): thread: \(Thread.current)") return await Purchases.shared.checkTrialOrIntroDiscountEligibility(product: product) } // MARK: internal_purchase @MainActor private func internal_purchase(package: Package) async throws -> PurchaseResultData { debug_print("Logica::Store::internal_purchase: thread: \(Thread.current)") return try await Purchases.shared.purchase(package: package) } // MARK: internal_restore_purchases @MainActor @discardableResult private func internal_restore_purchases() async throws -> CustomerInfo { debug_print("Logica::Store::internal_restore_purchases: thread: \(Thread.current)") return try await Purchases.shared.restorePurchases() } // MARK: get_customer_info @MainActor private func get_customer_info() async throws -> CustomerInfo { debug_print("Logica::Store::get_customer_info: thread: \(Thread.current)") return try await Purchases.shared.customerInfo() } // MARK: get_offerings @MainActor private func get_offerings() async throws -> Offerings { debug_print("Logica::Store::get_offerings: thread: \(Thread.current)") return try await Purchases.shared.offerings() } @MainActor // MARK: handle_transaction private func handle_transaction(_ transaction: StoreTransaction) async { debug_print("Logica::Store::handle_transaction: thread: \(Thread.current)") if let product = LogicaStoreProduct(rawValue: transaction.productIdentifier) { switch product { case .stars_50, .stars_100, .stars_300, .stars_500: SharedStorage.shared.add_stars(product: product) default: break } } } // MARK: update_state @StoreActor private func update_state(_ customer_info: CustomerInfo) async { debug_print("Logica::Store::update_state: thread: \(Thread.current)") debug_print("Logica::Store::update_state: entitlements: \(customer_info.entitlements.all)") let isSubscribed_ = customer_info.entitlements[LogicaEntitlement.base_content_access.rawValue]?.isActive ?? false debug_print("Logica::Store::update_state: isSubscribed_: \(isSubscribed_)") await MainActor.run { isSubscribed = isSubscribed_ } } // MARK: startup @StoreActor private func startup() async { debug_print("Logica::Store::startup: thread: \(Thread.current)") do { let customer_info = try await get_customer_info() await update_state(customer_info) } catch { if let err = error as? ErrorCode { debug_print("Logica::Store::startup: err: \(err.description)") } } } // MARK: listen_for_updates @StoreActor private func listen_for_updates() async { debug_print("Logica::Store::listen_for_updates thread: \(Thread.current)") for await customer_info in Purchases.shared.customerInfoStream { debug_print("Logica::Store::listen_for_updates: new customerInfo recieved") await update_state(customer_info) } } } // MARK: - Public Interface extension Store { // MARK: requestProductsIfNeeded /// Request product data from Backend if data was not previous loaded. /// /// Uses StoreActor avoid conncurrent execution on different bacground threads @StoreActor public func requestProductsIfNeeded() async { debug_print("Logica::Store::requestProductsIfNeeded: thread: \(Thread.current)") let (excahnged, original) = StoreActor.compareExchange(&productRefreshStatus, expected: .none, desired: .inProgress) if excahnged { await MainActor.run { withAnimation { storeError = nil } } let retries = 1 for retry_ in 0.. StoreTransaction? { debug_print("Logica::Store::purchase: thread: \(Thread.current)") let result = try await internal_purchase(package: product) if !result.userCancelled, let transaction = result.transaction { await handle_transaction(transaction) } #if DEBUG let customer_info = result.customerInfo let isSubscribed_ = customer_info.entitlements[LogicaEntitlement.base_content_access.rawValue]?.isActive ?? false debug_print("Logica::Store::purchase: debug: isSubscribed_:\(isSubscribed_)") // reports isSubscribed_ false even on successfull purchase #endif return result.transaction } @inlinable // MARK: eligibleForIntro(product: Package) static public func eligibleForIntro(product: Package) async throws -> Bool { debug_print("Logica::Store::eligibleForIntro: thread: \(Thread.current)") debug_print("Logica::Store::eligibleForIntro: checking IntroOffer for \(product.storeProduct.productIdentifier)") let eligibility_status = await internal_check_trial(product: product.storeProduct) debug_print("Logica::Store::eligibleForIntro: status: \(eligibility_status)") return eligibility_status == .eligible } @inlinable // MARK: eligibleForIntro(product: LogicaStoreProduct) static public func eligibleForIntro(product: LogicaStoreProduct) async throws -> Bool { debug_print("Logica::Store::eligibleForIntro: thread: \(Thread.current)") debug_print("Logica::Store::eligibleForIntro: checking IntroOffer for \(product)") let eligibility_infos = await internal_check_trial(productIdentifiers: [product.rawValue]) let eligibility_info = eligibility_infos[product.rawValue]! debug_print("Logica::Store::eligibleForIntro: status: \(eligibility_info.description)") return eligibility_info.status == .eligible } @MainActor @inlinable // MARK: openSubscriptionsManager static public func openSubscriptionsManager() async -> Void { debug_print("Logica::Store::openSubscriptionsManager thread: \(Thread.current)") do { try await Purchases.shared.showManageSubscriptions() } catch { debug_print("Logica::Store::openSubscriptionsManager: error: \(error.localizedDescription)") } } @StoreActor @inlinable // MARK: restorePurchases public func restorePurchases() async throws -> Void { debug_print("Logica::Store::restorePurchases: thread: \(Thread.current)") try await internal_restore_purchases() } // MARK: sortByPrice private func sortByPrice(_ products: [Package]) -> [Package] { products.sorted(by: { return $0.storeProduct.price < $1.storeProduct.price }) } // MARK: tier func tier(for product: LogicaStoreProduct) -> SubscriptionTier { return Store.subscriptionTier[product, default: .none] } } ```
run trace: ``` Thread 9 Queue : Backend callbackQueue (serial) #0 0x0000000103058938 in closure #1 in DeviceCache.cache(customerInfo:appUserID:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Caching/DeviceCache.swift:124 #1 0x00000001030e04f0 in closure #1 in SynchronizedUserDefaults.write(_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Misc/SynchronizedUserDefaults.swift:35 #2 0x00000001030e055c in partial apply for closure #1 in SynchronizedUserDefaults.write(_:) () #3 0x00000001030cf6ac in closure #1 in Atomic.withValue<τ_0_0>(_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Misc/Atomic.swift:71 #4 0x00000001030cf748 in partial apply for closure #1 in Atomic.withValue<τ_0_0>(_:) () #5 0x00000001030d1960 in Lock.perform<τ_0_0>(_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Misc/Lock.swift:30 #6 0x00000001030cefa0 in Atomic.withValue<τ_0_0>(_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Misc/Atomic.swift:70 #7 0x00000001030e0460 in SynchronizedUserDefaults.write(_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Misc/SynchronizedUserDefaults.swift:34 #8 0x00000001030587f0 in DeviceCache.cache(customerInfo:appUserID:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Caching/DeviceCache.swift:122 #9 0x00000001030954d4 in CustomerInfoManager.cache(customerInfo:appUserID:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Identity/CustomerInfoManager.swift:155 #10 0x0000000103093a28 in closure #1 in closure #1 in CustomerInfoManager.fetchAndCacheCustomerInfo(appUserID:isAppBackgrounded:completion:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Identity/CustomerInfoManager.swift:47 #11 0x0000000103112798 in CustomerInfoResponseHandler.handle(customerInfoResponse:completion:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/Operations/Handling/CustomerInfoResponseHandler.swift:32 #12 0x000000010310b0e8 in closure #1 in closure #2 in GetCustomerInfoOperation.getCustomerInfo(completion:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/Operations/GetCustomerInfoOperation.swift:68 #13 0x00000001b4e553d8 in Sequence.forEach(_:) () #14 0x00000001030e609c in closure #1 in CallbackCache.performOnAllItemsAndRemoveFromCache(withCacheable:_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/Caching/CallbackCache.swift:52 #15 0x00000001030e61bc in thunk for @callee_guaranteed () -> () () #16 0x00000001030e621c in thunk for @escaping @callee_guaranteed () -> () () #17 0x000000010467a7bc in _dispatch_client_callout () #18 0x000000010468bca8 in _dispatch_lane_barrier_sync_invoke_and_complete () #19 0x00000001030e5e38 in CallbackCache.performOnAllItemsAndRemoveFromCache(withCacheable:_:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/Caching/CallbackCache.swift:47 #20 0x000000010310b020 in closure #2 in GetCustomerInfoOperation.getCustomerInfo(completion:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/Operations/GetCustomerInfoOperation.swift:67 #21 0x00000001030ee82c in closure #1 in HTTPClient.Request.init<τ_0_0>(httpRequest:headers:completionHandler:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/HTTPClient.swift:93 #22 0x00000001030f152c in HTTPClient.handle(urlResponse:request:urlRequest:data:error:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/HTTPClient.swift:237 #23 0x00000001030f24ac in closure #1 in HTTPClient.start(request:) at /Users/xxx/Library/Developer/Xcode/DerivedData/Project-xxx/SourcePackages/checkouts/purchases-ios/Sources/Networking/HTTPClient.swift:281 ```
NachoSoto commented 2 years ago

Thanks for the detailed info, I'm looking into this right now. Another question: do you use @AppStorage anywhere in your app?

ZeeWanderer commented 2 years ago

Yes, i do use @AppStorage.

NachoSoto commented 2 years ago

Okay, so looks like your issue (@ZeeWanderer) and the original issue (@quantamrhino) are both due to similar things. If you don't specify a custom UserDefaults when initializing Purchases, it uses UserDefaults.standard. Internally, the SDK writes to it from backgrounds threads (which is fine, because it's thread-safe). However, it seems like @AppStorage isn't thread-safe, and possibly the original issue is affected by something similar (maybe by using KVO / UserNotifications and not managing this thread safety correctly.

The solution long term for the SDK is likely to change the default UserDefaults to a private one. In the mean time, I recommend working around this by specifying a custom UserDefaults(suiteName: "your_app_group").

NachoSoto commented 2 years ago

Another solution is to specify a different UserDefaults for @AppStorage:

@AppStorage("your_key", store: UserDefaults(suiteName: "your_app_group")) 
NachoSoto commented 2 years ago

@ZeeWanderer what version of iOS are you seeing the issue with @AppStorage though? I did this quick test and I can't reproduce it, even doing this dubious change from a background thread works fine with no warnings:

struct ContentView: View {
    @AppStorage("com.nachosoto.clicks")
    private var clicks: Int = 0

    var body: some View {
        VStack {
            Text("Number of clicks: \(self.clicks)")

            Button("Click me") {
                self.clicks = self.clicks + 1
            }

            Button("Do something in the background") {
                DispatchQueue(label: "test").async {
                    UserDefaults.standard.setValue(0, forKey: "com.nachosoto.clicks")
                    UserDefaults.standard.synchronize()
                }
            }
        }
    }
}
ZeeWanderer commented 2 years ago

iOS 15.4.1

alfredcc commented 2 years ago
Crashed: com.apple.main-thread
0  libswiftCore.dylib             0x37e84 _assertionFailure(_:_:file:line:flags:) + 308
1  Grow                           0x9b8fb4 closure #1 in variable initialization expression of static FatalErrorUtil.defaultFatalErrorClosure + 20 (FatalErrorUtil.swift:20)
2  Grow                           0x96ed1c DeviceCache.handleUserDefaultsChanged(notification:) + 32 (FatalErrorUtil.swift:32)
3  Grow                           0x96ed8c @objc DeviceCache.handleUserDefaultsChanged(notification:) + 4386565516 (<compiler-generated>:4386565516)
4  CoreFoundation                 0x2ab34 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 148
5  CoreFoundation                 0xc4e28 ___CFXRegistrationPost_block_invoke + 88
6  CoreFoundation                 0x98284 _CFXRegistrationPost + 440
7  CoreFoundation                 0x40844 _CFXNotificationPost + 704
8  Foundation                     0x185a4 -[NSNotificationCenter postNotificationName:object:userInfo:] + 92

`crash_info_entry_0

RevenueCat/DeviceCache.swift:71: Fatal error: [Purchases] - Cached appUserID has been deleted from user defaults. This leaves the SDK in an undetermined state. Please make sure that RevenueCat entries in user defaults don't get deleted by anything other than the SDK. More info: https://rev.cat/userdefaults-crash`

alfredcc commented 2 years ago

Same issue? All crash is on iOS 16, And we use the last SDK version. We use custom UserDefault Group

alfredcc commented 2 years ago

Here is my init code

        // https://dev.appsflyer.com/hc/docs/integrate-ios-sdk
        AppsFlyerLib.shared().appsFlyerDevKey = Consts.appsFlyerDevKey
        AppsFlyerLib.shared().appleAppID = Consts.appleAppID

        #if DEBUG
            Purchases.logLevel = .debug
        #endif

        /*
         Initialize the RevenueCat Purchases SDK.

         - `appUserID` is nil by default, so an anonymous ID will be generated automatically by the Purchases SDK.
            Read more about Identifying Users here: https://docs.revenuecat.com/docs/user-ids

         - `observerMode` is false by default, so Purchases will automatically handle finishing transactions.
            Read more about Observer Mode here: https://dz gocs.revenuecat.com/docs/observer-mode
         */

        Purchases.configure(
            with: Configuration.Builder(withAPIKey: Consts.revenuecatKey)
                .with(usesStoreKit2IfAvailable: true)
                .with(userDefaults: UserDefaults.group)
                .build()
        )

        // https://docs.revenuecat.com/docs/appsflyer
        // Automatically collect the $idfa, $idfv, and $ip values
        Purchases.shared.collectDeviceIdentifiers()
        // You should make sure to set attributes after the Purchases SDK is configured, and before the first purchase occurs.
        // Set the Appsflyer Id
        Purchases.shared.setAppsflyerID(AppsFlyerLib.shared().getAppsFlyerUID())

It happened randomly, I know it's hard to fix this.🥲 But the crashing is still increasing. I hope someone can fix this later, Thank you.

NachoSoto commented 2 years ago

Thanks for the report! The additional details are helpful.

Can you provide more info about what else you do with UserDefaults.group? We are aware of this issue, and for now if you're using a custom UserDefaults instance we recommend not using that outside of RevenueCat.

alfredcc commented 2 years ago

Yet In UserDefaults.group, we also use it to share some data to widget extension. Thanks for the help we will try to use a single custom UserDefaults for RevenueCat later.

NachoSoto commented 2 years ago

Yes, separating your data into 2 UserDefaults should fix this 👍🏻

taquitos commented 2 years ago

I think this is solved, so I'm going to close it out. If this is still a problem for you, definitely feel free to re-open and let us know what you tried and the results.

github-actions[bot] commented 2 years ago

This issue has been automatically locked due to no recent activity after it was closed. Please open a new issue for related reports.