I was experimenting with the new core data API NSPersistentContainer and was under the impression that an internal queueing mechanism would prevent write transactions from evaluating concurrently, as detailed in this stack overflow answer NSPersistentContainer concurrency for saving to core data
The way that a lot of pros have been dealing with the problem for a long time (even before NSPersistentContainer did it) was to have an operation queue to queue the writes so there is only one write going on at a time, and have another context on the main thread only for reads. This way you never get any merge conflicts. (see https://vimeo.com/89370886 for a great explanation on this setup which is now what NSPersistentContainer does internally). When you call performBackgroundTask the persistentContainer enqueues that block into an internal serial queue. This ensure that there are no mergeConflicts.
However if I insert multiple entities which share a relationship destination in a tight loop using performBackgroundTask
for each iteration, I get a NSMergeConflict
error when I save the context:
let bundlePath = Bundle.main.resourceURL!
let directoryEnumerator = FileManager.default.enumerator(at: bundlePath, includingPropertiesForKeys: [URLResourceKey.isDirectoryKey, URLResourceKey.nameKey])
while let url = directoryEnumerator?.nextObject() as? URL {
if url.pathExtension == "jpeg" {
let imageData = try! Data(contentsOf: url)
DataManager.persistentContainer.performBackgroundTask { (context) in
// context.mergePolicy = NSMergePolicy.overwrite
let new = Photo(context: context)
new.name = url.lastPathComponent
new.data = imageData as NSData
let corresponding = try! context.existingObject(with: DataManager.rootFolder.objectID) as! Folder
new.parent = corresponding
try! context.save()
}
}
I posted a sample project on github to demonstrate the problem: https://github.com/MaximeBoulat/NSPersistentContainer_Merge_Conflict
The crash seems to be happening because multiple entities are setting their "parent" relationship concurrently, for the same parent, which causes the parent's "children" relationship to be desynchronized across concurrent updates.
This happens even if I set the .automaticallyMergesChangesFromParent
property of the incoming context to true. I can prevent the crash by defining the incoming context's merge policy, but that's not an acceptable solution.
Is there any way to configure NSPersistentContainer to properly serialize updates dispatched using the performBackgroundTask
API. Or is there something I am missing which is causing these updates to conflict with each other? Or did Apple provide the NSPersistentContainer stack with the expectations that any conflicts encountered when evaluating logic passed into performBackgroundTask
should either be fatal, or be disregarded?