Core Data crashes when attempting to establish relationship to an entity with derived attribute in the background

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:

  1. If I do not establish the relationship to Account from Transaction, the crash doesn't happen; if I uncheck derived on the balance attribute of Account, the crash also does not happen.
  2. 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%.
  3. I have double checked all concurrency usages and all operations are performed within NSPersistentCloudKitContainer's performBackgroundTaskmethod, 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.
  4. 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!

Answered by DTS Engineer in 797377022

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.

Submitted TSI case ID 8425387

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.

@DTS Engineer Thanks for your respsonse. I have filed feedback FB14508189.

I was able to find a workaround by creating the background context as a child to the view context, in contrast to as a standalone context. I would then perform operations on this child context, save it, then save the view context. This seems to avoid the crash.

Can you comment on the performance difference between this approach and saving on a standalone background context then wait for the system to auto merge changes to the view context?

Core Data crashes when attempting to establish relationship to an entity with derived attribute in the background
 
 
Q