I have recently moved some of my data save operations from the view context to background contexts. Since the switch, some (but not all) of my users are reporting that the background save operations crash 100% of the time. After researching, I have narrowed it down to the fact that these save operations involve establishing a relationship to an entity with a derived attribute. An example is shown below:
try await CoreDataStack.shared.performBackgroundTask { context in
var transaction = Transaction(context: context)
transaction.amount = NSDecimalNumber(decimal: 0)
transaction.id = UUID()
let account = Account.account(withName: "Default", in: context)
transaction.account = account
try context.save()
} // <= Crashes!
In the above example, each Transaction
has a to-one relationship to an Account
, and the latter has a to-many relationship the the former.
Account
has a derived attribute called balance
that is calculated using the expression sum:(transactionItems.amount)
.
The would then crash when the performBackgroundTask
block exits (after the save()
operation returns):
Thread 4 Crashed:
0 libobjc.A.dylib 0x00000001850ae00c objc_release_x8 + 8
1 CoreData 0x00000001900d8cfc -[_CDSnapshot dealloc] + 72 (_CDSnapshot.m:691)
2 CoreData 0x00000001900d8b68 _NSQLRow_dealloc_standard + 48 (NSSQLRow.m:156)
3 CoreFoundation 0x0000000187d72228 __CFBasicHashRemoveValue + 192 (CFBasicHash.c:1332)
4 CoreFoundation 0x0000000187d7212c CFBasicHashRemoveValue + 452 (CFBasicHash.c:1418)
5 CoreFoundation 0x0000000187d71638 CFDictionaryRemoveValue + 196 (CFDictionary.c:477)
6 CoreData 0x00000001900f6fa8 -[NSPersistentStoreCache decrementRefCountForObjectID:] + 96 (NSPersistentStoreCache.m:120)
7 CoreData 0x00000001900f6eec -[NSSQLCore managedObjectContextDidUnregisterObjectsWithIDs:generation:] + 172 (NSSQLCore.m:4329)
8 CoreData 0x00000001900f30f4 0x1900d6000 + 119028
9 CoreData 0x00000001900f4d74 gutsOfBlockToNSPersistentStoreCoordinatorPerform + 204 (NSPersistentStoreCoordinator.m:404)
10 libdispatch.dylib 0x000000018fe9b0d8 _dispatch_client_callout + 20 (object.m:576)
11 libdispatch.dylib 0x000000018fea26e0 _dispatch_lane_serial_drain + 744 (queue.c:3934)
12 libdispatch.dylib 0x000000018fea31e8 _dispatch_lane_invoke + 380 (queue.c:4025)
13 libdispatch.dylib 0x000000018feae258 _dispatch_root_queue_drain_deferred_wlh + 288 (queue.c:7185)
14 libdispatch.dylib 0x000000018feadaa4 _dispatch_workloop_worker_thread + 532 (queue.c:6779)
15 libsystem_pthread.dylib 0x000000020f3c0c7c _pthread_wqthread + 288 (pthread.c:2696)
16 libsystem_pthread.dylib 0x000000020f3bd488 start_wqthread + 8
performBackgroundTask
is defined as:
func performBackgroundTask<T>(_ block: @escaping (NSManagedObjectContext) throws -> T) async rethrows -> T {
try await container.performBackgroundTask { context in
context.transactionAuthor = appTransactionAuthorName
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return try block(context)
}
}
Things to note:
- If I do not establish the relationship to
Account
fromTransaction
, the crash doesn't happen; if I uncheckderived
on thebalance
attribute ofAccount
, the crash also does not happen. - For those users who are experiencing the crash, the crash rate is 100%; for those who aren't experiencing the crash, the crash never happens. So it's either 100% or 0%.
- I have double checked all concurrency usages and all operations are performed within
NSPersistentCloudKitContainer
'sperformBackgroundTask
method, so it's probably not a threading issue. I have also tried ASan, Core Data's concurrency debug, as well as Zombie objects to no avail. - The crash seems to be related to a user's existing data. While I couldn't reproduce the crash on my own database, once I replaced the underlying SQLite file with one of my crashing user's, the crash is 100% reproducible.
I am at my wit's end here. Is this an internal Core Data bug, or am I doing something incorrectly? Any help is greatly appreciated!
The stack trace shows that the crash was triggered by -[_CDSnapshot dealloc]
when the system exited the private queue tied to the background managed object context.
-[_CDSnapshot dealloc]
releases an NSManagedObjectID
instance it holds and the properties it caches, and deallocates the snapshot object. What line #691 at _CDSnapshot.m
does is basically deallocating self
.
So it seems clear to me that the crash was triggered by an over-release of a _CDSnapshot
object, and I believe that is a bug on the system side because:
-
Snapshots are completely managed by Core Data. There is no way for apps to manipulate snapshots.
-
The code snippets you share look good to me.
-
You have "tried ASan, Core Data's concurrency debug, as well as Zombie objects to no avail."
What I can suggest here is that you file a feedback report for the Core Data team to investigate, and share your feedback report ID here, if you don't mind, so folks who encountering the same issue can track.
Since you can avoid the crash by performing the task in the main queue context or by not using the derived property, I hope the issue doesn't block your development.
Best,
——
Ziqiao Chen
Worldwide Developer Relations.