I recently managed to catch an extremely rare exception in my code and was hoping folks here could help me understand it. I have a game which is tracking elapsed time and persisting the value to Core Data every second. At the same time a user could be playing the game and causing the score to update, which is also saved in Core Data. I suspect that I should be serializing saves, but at the moment I'm not. Everything works great 99.99% of the time, but once in a while the call to context.save() throws an exception.
I have a merge policy set as so:
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
And here's how I save elapsed time. The same pattern is used for updating a player's score.
func saveElapsedTime() {
let container = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
container.performBackgroundTask() { (context) in
let storedGame = context.object(with: self.storedGameID) as! StoredGame
storedGame.elapsedTime = Int32(self.elapsedTime)
do {
try context.save()
} catch let nserror as NSError {
print("saveElapsedTime: failed to save: \(nserror). userInfo: \(nserror.userInfo).")
}
}
}
The rare error I was able to catch looks like this:
Error Domain=NSCocoaErrorDomain Code=133020 "Could not resolve merge changes."
<snip>...with oldVersion = 463 and newVersion = 463...<snip>
I've seen plenty of posts where the old and new versions are off by one or a small number, and people recommend setting a merge policy which is different than the default. I'm guessing that in my case where oldVersion == newVersion it's because multiple threads are trying to save at the exact same time. Can someone confirm this just by looking at the error message? Should I be serializing all saves using an OperationQueue like in this example: NSPersistentContainer concurrency for saving to core data?
Thanks in advance.
Related
My app is crashing whenever I try to save the Core Data MOC with a "Could Not Merge Changes" error. The baffling part to me is that it only occurs with a SQLite file that I seed to the app upon initial launch when the SQLite file has 40k+ records on one of the tables. The data displays perfectly fine in tableViews and I can update existing objects. I just can't save any new objects without crashing.
I've taken the same SQLite file (to maintain the same schema), removed all records, added back in a small quantity of records on the tables and everything works fine in the app -- I can move between view controllers, add new objects to the Core Data entities, and save the MOC anywhere.
I'm using a Core Data stack in a singleton. I've logged each request for the MOC and there is only one on the main thread. The app is very basic with no background tasks running. My build target is iOS 13.0.
The error is also confusing because the error isn't even on the new object that I'm inserting into Core Data. It is for some other existing object in Core Data and is different each time it crashes (For instance this would occur when I add a BetaCat or DeltaCat object):
Fatal error: Unresolved error Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x600000025480) for NSManagedObject (0x600003661900) with objectID '0xe3afe1928ab74f71 <x-coredata://E62171B1-E38F-4BF5-B012-DB1842171304/TextCategory/p4>' with oldVersion = 0 and newVersion = <deleted> and old cached row = {\n category = AlphaCat;\n isLocked = \"<null>\";\n}"
), NSExceptionOmitCallstacks=true}, ["conflictList": <__NSArrayM 0x6000007e35d0>(
NSMergeConflict (0x600000025480) for NSManagedObject (0x600003661900) with objectID '0xe3afe1928ab74f71 <x-coredata://E62171B1-E38F-4BF5-B012-DB1842171304/TextCategory/p4>' with oldVersion = 0 and newVersion = <deleted> and old cached row = {
category = AlphaCat;
isLocked = "<null>";
I've read everything that I can find on the merge change error and it almost always seems to be an issue related to multiple MOCs trying to save data and there being conflicts which doesn't seem to apply to my situation.
Does anyone have any idea why there are issues with the larger SQLite file and not the smaller one?
EDIT
Code example of how I get a MOC, set entity attributes, and save MOC:
let newCategory = Category(context: CoreDataStack.getContext())
newCategory.attribute1 = textField.text!
newCategory.attribute2 = true
CoreDataStack.saveContext()
getContext() simply returns a persistentContainer.viewContext
and saveContext() is a basic:
if context.hasChanges {
do {
try context.save()
} catch let error as NSError {
let nserror = error as NSError
fatalError("Unresolved error when saving MOC: \(nserror), \(nserror.userInfo)")
}
}
If your context is shared, it is best to set its mergePolicy in advance, like this:
yourContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy//OR: NSMergeByPropertyObjectTrumpMergePolicy
Note:
This post does not apply since I actually use CoreData.
In this post the last answer suggests to fetch all items in the new background thread before adding new objects, but this is done in my code.
This post suggests to unfault an item before its context is saved, but this is also done in my code.
My app uses CoreData to store objects called shoppingItems. I wrote a class CoreDataManager that initialises CoreData and has essentially one function to overwrite the currently stored items, and one function to fetch all items. Both functions operate in the background, i.e. on a separate thread.
Here is my code (irrelevant parts are left out).
I setup core data on the main thread:
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
})
return container
}()
This is the write function:
func overwriteShoppingItems(_ shoppingItems: Set<ShoppingItem>, completion: #escaping (Error?) -> Void) {
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let viewContext = self.persistentContainer.viewContext
backgroundContext.parent = viewContext
backgroundContext.performAndWait {
// Delete currently stored shopping items
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: CDEntityShoppingItem)
do {
let result = try backgroundContext.fetch(fetchRequest)
let resultData = result as! [NSManagedObject]
for object in resultData {
backgroundContext.delete(object)
}
if !shoppingItems.isEmpty {
// Save shopping items in managed context
let cdShoppingItemEntity = NSEntityDescription.entity(forEntityName: CDEntityShoppingItem, in: backgroundContext)!
for nextShoppingItem in shoppingItems {
let nextCdShoppingItem = CDShoppingItem(entity: cdShoppingItemEntity, insertInto: backgroundContext)
nextCdShoppingItem.name = nextShoppingItem.name
}
}
let saveError = self.saveManagedContext(managedContext: backgroundContext)
completion(saveError)
} catch let error as NSError {
completion(error)
}
}
}
func saveManagedContext(managedContext: NSManagedObjectContext) -> Error? {
if !managedContext.hasChanges { return nil }
do {
try managedContext.save()
return nil
} catch let error as NSError {
return error
}
}
And this is the fetch function:
func fetchShoppingItems(completion: #escaping (Set<ShoppingItem>?, Error?) -> Void) {
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let viewContext = self.persistentContainer.viewContext
backgroundContext.parent = viewContext
backgroundContext.performAndWait {
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
do {
let cdShoppingItems: [CDShoppingItem] = try backgroundContext.fetch(fetchRequest)
guard !cdShoppingItems.isEmpty else {
completion([], nil)
return
}
for nextCdShoppingItem in cdShoppingItems {
}
completion(shoppingItems, nil)
} catch let error as NSError {
completion(nil, error)
}
}
}
In normal operation, the code seems to work.
The problem:
I also wrote a unit test that tries to provoke multi-threading problems. This test uses a concurrent dispatch queue:
let concurrentReadWriteQueue = DispatchQueue(label: „xxx.test_coreDataMultithreading", attributes: .concurrent)
A timer defines the test time.
In the scheme for tests, I have set the arguments -com.apple.CoreData.Logging.stderr 1 and -com.apple.CoreData.ConcurrencyDebug 1.
During the test time, overwriteShoppingItems and fetchShoppingItems are inserted repeatedly in the queue, and executed concurrently.
This unit test executes a few reads and writes, before it stops at the line
let itemName = nextCdShoppingItem.name!
because nextCdShoppingItem.name is nil, which should never happen, because I never store nil.
Immediately before the crash, the following is logged:
CoreData: error: API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x600000e6c980, store PSC = 0x0)
If I do only fetches, or only writes, the CoreData warning is not logged. Thus it seems definitely to be a multi-threading issue.
However, CoreData.ConcurrencyDebug does not detect it.
It looks as if during a fetch operation on one thread, the other thread deletes the currently fetched item, so that its properties are read back as nil.
But this should not happen because fetches and saves are done with backgroundContext.performAndWait, i.e. serially.
And the stack trace shows that only a single thread accesses CoreData: Thread 3 Queue : NSManagedObjectContext 0x600003c8c000 (serial)
My questions:
What is my misuse of the CoreData API, and how to do it right?
Why is an item sometimes read back as nil?
EDIT:
Maybe this helps to identify the problem: When I comment out backgroundContext.delete(object) in overwriteShoppingItems, the error is no longer logged, and no item is fetched as nil.
Okay so i got same error but only while "archiving" the project. Took me several hours to find the issue, since I was getting the error only on uploading, in the assets catalog, I had some duplicate images, by removing them the error is gone.
Anyone else getting this error AND your Coredata setup is fine, check your assets folder.
If you are not using core data in your project then check your assets files are proper or not.
I am also getting error like this then i have findout this error from my project.
Problem seems to be solved.
It happened apparently, because the functions overwriteShoppingItems and fetchShoppingItems both setup a separate background context with let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) with their own queue so that fetches and saves were not serialized by a single queue.
I modified my code now in the following way:
I have now a property
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
that is initialized as
self.backgroundContext.persistentStoreCoordinator = self.persistentContainer.persistentStoreCoordinator
self.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
and fetches and saves are done using
backgroundContext.perform {…}
Now the CoreData error is no longer logged, and no item is fetched as nil.
In my case I created an new managedObjectContext without (yet) assigning a persistent store. Eventually I called -[psc metadataForPersistentStore:store] with the store being nil. This caused the log message to be written.
(side-note: you can always catch these outputs by setting a breakpoint on fprintf)
If you are seeing this during tear-down of a Core Data stack, check for un-removed observers or memory leaks.
In my case, I had managed objects which were each observing themselves – one of their relationships, using Swift's Key-Value Observing (NSKeyValueObservation objects). (Example use case: When the last a department's employees are deleted, delete the department.) The console messages appeared while the containing Core Data document was being closed. By breaking on fprintf I could see that the message was logged when a notification was being sent to an observer, and the number of messages was always equal to the number of non-faulted such objects in the managed object context (5 departments --> 5 console messages, etc., etc.). The problem was that I was not removing these observations, and the solution was of course to implement func willTurnIntoFault() in the observing objects, and in this func remove the observation.
In another situation, these messages stopped after I fixed a memory leak, but I did not study that in detail.
The full error messages were: error: API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x600002c5cc00, store PSC = 0x0)
CoreData: error: API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x600002c5cc00, store PSC = 0x0) and they occurred during the call to NSPersistentStoreCoordinator.remove(_:). So, this error is telling me that the store's PSC was nil, but I checked this with the debugger and found that the store's PSC was not nil before the call. I filed a bug to Apple on this (FB11669843) asking if they could improve the error message.
I need to handle a core data crash. My code got crashed on the managedObjectContext.save().
But catch block did not catch any exception. To avoid the crash how can I write my Catch block better Here is my code.
do {
try managedObjectContext.save()
}
catch let error as NSError {
Print.print("Error saving data store: \(error)")
}
This is the sample for saving data using CoreData .This may helps you .
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
if let entity = NSEntityDescription.entity(forEntityName: "Employees", in: context){
let myItem = NSManagedObject(entity: entity, insertInto: context)
myItem.setValue(nameTF.text, forKey: "names")
myItem.setValue(15655, forKey: "mobileNo")
do {
try context.save()
}catch let nserror as NSError{
print("ERROR: Coredata error \(nserror)")
}
}
This really looks like you messed up with your initialization of context, persistent store and its coordinator. You would do best looking into that. There are similar posts already on SO like this one.
More importantly you will not intercept such exception with try-catch in Swift. Realistically the Swift try-catch does not have anything to do with exceptions but is a high level API for you to intercept reported errors. In your case you just intercept the error that may be reported when saving your data into database. But the error came from a bit deeper as it seems.
To go a step further the whole core data is still completely in objectiveC which has completely different system for throwing exceptions and although those exceptions MAY be intercepted with objectiveC try-catch the same exception will not be intercepted by the one from Swift. What this system did is only replaced entering of pointer to error into method: .save(&error) which was used in objectiveC. And your catch block will trigger only when this error is non-null.
Yes of course I did search in the whole internet .
But can't get out of this issue.
I got two entity named:
Post & PopularPost.
(Almost duplicate of one another)
When I fetch the Post and update it's properties like numberoflikes,numberofcomments it's good.
But when I fetch the PopularPost and try to update it's properties then it says
"optimistic locking failure"
MY code to fetch and update and save the entity:"PopularPosts".
let postpopFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "PopularPosts")
postpopFetch.predicate = NSPredicate(format: "id = %#", postId)
let resultp = try? CoreDataManager.sharedInstance.managedObjectContext.fetch(postpopFetch)
let resultDatap = resultp as! [PopularPosts]
for object in resultDatap {
print(object.numberOfHearts)
if like {
object.numberOfHearts = object.numberOfHearts + 1
object.isLiked = true
}else{
(object.numberOfHearts > 0) ? (object.numberOfHearts = object.numberOfHearts - 1) : (object.numberOfHearts = 0)
object.isLiked = false
}
}
do {
try CoreDataManager.sharedInstance.managedObjectContext.save()
print("saved!")
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
Generally optimistic locking failure is caused by two different managed object contexts trying to change the same data. I think you can also have this issue even with one context if you are inappropriately accessing it from different threads at the same time. ManagedObjectContexts are not
thread safe neither for reading or for writing.
I have seen situation that there is access to a managedObjectContext from the wrong thread and there is a crash much later when on a line of code that is doing nothing wrong. I would recommend to careful search your code for any access to core-data that is not on the main thread. You use [NSThread isMainThread] to check if you are not on the main thread for debugging.
In my case this was caused by using a newBackgroundContext().
I had a potentially long running task which started when the app went into the background.
Even though it completed before using the app again, I found that all saves made after the background task would fail.
I solved the issue by using the viewContext for the long running task.
I'm parsing data from a JSON file that has approximately 20000 objects. I've been running the time profiler to figure out where my bottlenecks are and speed up the parse and I've managed to reduce the parse time by 45%, however according to the time profiler 78% of my time is being taken by the context.save() and much of the heavy portions throughout the parse are sourcing from where I call NSEntityDescription.insertNewObjectForEntityForName.
Does anyone have any idea if theres any way to speed this up? I'm currently batching my saves every 5000 objects. I tried groupings of 100,1000,2000,5000,10000 and I found that 5000 was the most optimal on the device I'm running. I've read through the Core Data Programming Guide but have found most of the advice it gives is to optimizing fetching on large numbers of data and not parsing or inserting.
The answer could very well be, Core Data has its limitations, but I wanted to know if anyone has found ways to further optimize inserting thousands of objects.
UPDATE
As requested some sample code on how I handle parsing
class func parseCategories(data: NSDictionary, context: NSManagedObjectContext, completion: ((success: Bool) -> Void)) {
let totalCategories = data.allValues.count
var categoriesParsed = 0
for (index, category) in data.allValues.enumerate() {
let privateContext = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)
privateContext.persistentStoreCoordinator = (UIApplication.sharedApplication().delegate as! AppDelegate).persistentStoreCoordinator!
privateContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
//Do the parsing for this iteration on a separate background thread
privateContext.performBlock({ () -> Void in
guard let categoryData = category.valueForKey("category") as? NSArray else{
print("Fatal Error: could not parse the category data into an NSArray. This should never happen")
completion(success: false)
return
}
let newCategory: Categories?
do {
let newCategory = NSEntityDescription.insertNewObjectForEntityForName("Categories", inManagedObjectContext: privateContext) as! Categories
newCategory.name = category.valueForKey("name") as? String ?? ""
newCategory.sortOrder = category.valueForKey("sortOrder") as? NSNumber ?? -1
SubCategory.parseSubcategories(category.valueForKey("subcategories") as! NSArray, parentCategory: newCategory, context: privateContext)
} catch {
print("Could not create the Category object as expected \(error)")
completion(success: false)
}
do {
print("Num Objects Inserted: \(privateContext.insertedObjects.count)") //Num is between 3-5k
try privateContext.save()
} catch {
completion(success: false)
return
}
categoriesParsed+=1
if categoriesParsed == totalCategories{
completion(success: true)
}
})
}
}
In the above code, I look through the top level data objects which I call a "Category", I spin off background threads for each object to parse concurrently. There are only 3 of this top level object, so it doesn't get too thread heavy.
Each Category has SubCategories, and several other levels of child objects which yield several thousand objects each getting inserted.
My core data stack is configured with one sqlite database the standard way that is configured when you create an app with CoreData
One reason is that you're saving the managed object context in each single iteration, which is expensive and not needed. Save it after the last item has been inserted.