I have a UITableView that fetches data from CoreData using FetchedResultsController and it registers for the data update.
On a second thread, I download the data from the server and update the same data (that's used by the UITableView). The update is not complicated and It is just updating a BOOL field of the entity.
When I call the save on Object Context, I get this exception: NSInternalInconsistencyException and the reason is
"Failed to process pending changes before save. The context is still dirty after 100 attempts. ..."
If I do not save right after the update but only at the time when the application is about to terminate, the application runs fine and the UITableView is correctly updated and the data is persisted.
Any pointer on why that might be happening? Am I doing something wrong?
Managed object contexts are not thread safe. Do you have a separate MOC for each thread?
If so, I believe the correct pattern is to register for NSManagedObjectDidSaveNotifications from the background MOC such that you can do a mergeChangesFromContextDidSaveNotification on the main MOC (from the main thread). This will keep your MOCs in sync; it does not happen automatically.
Related
I'm building my first iOS app, which in theory should be pretty straightforward but I'm having difficulty making it sufficiently bulletproof for me to feel confident submitting it to the App Store.
Briefly, the main screen has a table view, upon selecting a row it segues to another table view that displays information relevant for the selected row in a master-detail fashion. The underlying data is retrieved as JSON data from a web service once a day and then cached in a Core Data store. The data previous to that day is deleted to stop the SQLite database file from growing indefinitely. All data persistence operations are performed using Core Data, with an NSFetchedResultsController underpinning the detail table view.
The problem I am seeing is that if you switch quickly between the master and detail screens several times whilst fresh data is being retrieved, parsed and saved, the app freezes or crashes completely. There seems to be some sort of race condition, maybe due to Core Data importing data in the background whilst the main thread is trying to perform a fetch, but I'm speculating. I've had trouble capturing any meaningful crash information, usually it's a SIGSEGV deep in the Core Data stack.
The table below shows the actual order of events that happen when the detail table view controller is loaded:
Main Thread Background Thread
viewDidLoad
Get JSON data (using AFNetworking)
Create child NSManagedObjectContext (MOC)
Parse JSON data
Insert managed objects in child MOC
Save child MOC
Post import completion notification
Receive import completion notification
Save parent MOC
Perform fetch and reload table view
Delete old managed objects in child MOC
Save child MOC
Post deletion completion notification
Receive deletion completion notification
Save parent MOC
Once the AFNetworking completion block is triggered when the JSON data has arrived, a nested NSManagedObjectContext is created and passed to an "importer" object that parses the JSON data and saves the objects to the Core Data store. The importer executes using the new performBlock method introduced in iOS 5:
NSManagedObjectContext *child = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[child setParentContext:self.managedObjectContext];
[child performBlock:^{
// Create importer instance, passing it the child MOC...
}];
The importer object observes its own MOC's NSManagedObjectContextDidSaveNotification and then posts its own notification which is observed by the detail table view controller. When this notification is posted the table view controller performs a save on its own (parent) MOC.
I use the same basic pattern with a "deleter" object for deleting the old data after the new data for the day has been imported. This occurs asynchronously after the new data has been fetched by the fetched results controller and the detail table view has been reloaded.
One thing I am not doing is observing any merge notifications or locking any of the managed object contexts or the persistent store coordinator. Is this something I should be doing? I'm a bit unsure how to architect this all correctly so would appreciate any advice.
Pre-iOS 5, we've usually had two NSManagedObjectContexts: one for the main thread, one for a background thread. The background thread can load or delete data and then save. The resulting NSManagedObjectContextDidSaveNotification was then passed (as you're doing) to the main thread. We called mergeChangesFromManagedObjectContextDidSaveNotification: to bring those in to the main thread context. This has worked well for us.
One important aspect of this is that the save: on the background thread blocks until after the mergeChangesFromManagedObjectContextDidSaveNotification: finishes running on the main thread (because we call mergeChanges... from the listener to that notification). This ensures that the main thread managed object context sees those changes. I don't know if you need to do this if you have a parent-child relationship, but you did in the old model to avoid various kinds of trouble.
I'm not sure what the advantage of having a parent-child relationship between the two contexts is. It seems from your description that the final save to disk happens on the main thread, which probably isn't ideal for performance reasons. (Especially if you might be deleting a large amount of data; the major cost for deletion in our apps has always happened during final save to disk.)
What code are you running when the controllers appear/disappear that could be causing core data trouble? What kinds of stack traces are you seeing the crash on?
Just an architectural idea:
With your stated data refresh pattern (once a day, FULL cycle of data deleted and added), I would actually be motivated to create a new persistent store each day (i.e. named for the calendar date), and then in the completion notification, have the table view setup a new fetchedresultscontroller associated with the new store (and likely a new MOC), and refresh using that. Then the app can (elsewhere, perhaps also triggered by that notification) completely destroy the "old" data store. This technique decouples the update processing from the data store that the app is currently using, and the "switch" to the new data might be considered dramatically more atomic, since the change happens simply be starting to point to the new data instead of hoping you aren't catching the store in an inconsistent state while new data is being written (but is not yet complete).
Obviously I have left some details out, but I tend to think that much data being changed while being used should be re-architected to reduce the likelihood of the kind of crash you are experiencing.
Happy to discuss further...
NSFetchedResultsController has been proven to be a bit sensitive to massive deletes so that is where I would start digging first.
My initial question is, how are the re-fetch and reload of tableview related to the start of delete operation. Is there a chance that the deletion block will save child MOC while the NSFetchedResultsController is still fetching or no?
Is it possible that when you switch from detail view to master and then back to detail view there will be multiple concurrent background tasks running? Or are you retrieving all the data from the web service at once not only that relevant to a particular row?
One alternative to make this more robust is to use a pattern similar to what UIManagedDocument uses:
Instead of using a parent MOC as Main Thread concurrency type, UIManagedDocument actually creates the main MOC as private queue and makes the child MOC available to you use on the main thread. The benefit here is that all the I/O goes on in the background and saves to the parent MOC does not interfere with the child MOC at all until child MOC is explicitly made know about them. That's because save commits changes from child to parent and not the other way around.
So if you did your deletes on a parent queue which is private, that would not end up in the NSFetchedResultsController scope at all. And since it is old data, that is actually the preferred way.
An alternative I offer is to use three contexts:
Main MOC (NSPrivateQueueConcurrencyType)
Responsible for persistent store and deletion of old data.
Child MOC A (NSMainQueueConcurrencyType)
Responsible for anything UI related and NSFetchedResultsController
Child MOC B (NSPrivateQueueConcurrencyType, child of Child MOC A)
Responsible for inserting new data and committing it up to Child MOC
A when done.
The main issue I've had with multi-threaded core data is inadvertently accessing a managed object in a thread/queue other than the one it was created in.
I've found a good debugging tool is add NSAsserts to check that to managed objects created in your main managed object context are only used there, and ones created in a background context aren't used in the main context.
This will involve subclassing NSManagedObjectContext and NSManagedObject:
Add a iVar to the MOC subclass and assign to it the queue it was created on.
Your MO subclass should check the current queue is the same as its MOC's queue property.
It's only a few lines of code, but long term can prevent you making errors that are hard to track down otherwise.
[EDIT: simplified version of the question]
mainMOC is the primary managed object context
editorMOC is a managed object context created in editorViewController with an undo manager so the user can edit a single managed object
after editorMOC saves, mainMOC refreshes the updated managed object in the notification handler for NSManagedObjectContextDidSaveNotification
In the save handler, if I use [mainMOC refreshObject:obj mergeChanges:YES] the updates to the object are not reflected in mainMOC post-refresh. If I use [mainMOC refreshObject:obj mergeChanges:NO] the object is invalidated and at the next fault the changes are reflected in the data loaded from the store.
QUESTION: Why would the object not reflect the update when mergeChanges:YES is specified?
[ORIGINAL QUESTION]
I have a core data based app with multiple managed object contexts. The app is complicated and proprietary so I cannot simply share code directly from the app. I have created a simple test app in an attempt to reproduce my issue but the test app doesn't exhibit the problem. I have not been able to find a logical difference between the implementations. I apologize for not posting sample code, I believe I explained the implementation well below. If something is not clear, please ask in the comments and I will do my best to clarify.
Here's my situation. Everything described below is running on the main thread.
The app has a primary managed object context called mainMOC that is accessed on the main thread and used with NSFetchedResultsControllers to display data in various table views.
I have a view controller called EditorViewController that allows editing of an existing object of a particular entity. This view controller creates it's own managed object context called editorMOC using the same persistent store coordinator with an undo manager so changes can be rolled back or saved when dismissing the editor.
EditorViewController is observing the NSManagedObjectContextDidSaveNotification. When this notification occurs, the notification handler calls [_mainMOC mergeChangesFromContextDidSaveNotification:notification] to merge the changes from editorMOC into mainMOC.
The table view controller that uses an NSFetchedResultsController is handling the controller delegate messages.
I have added NSLog output to look at look at what happens in all of the above steps and I have verified the following:
I can see that the object is modified and saved in the editor.
I can see that the NSManagedObjectContextDidSaveNotification is called and that the updated object is included.
I can see that the fetched results controller is receiving the controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: protocol message.
I can see that mainMOC is not reflecting the updates and that the updated object has not been invalidated by mergeChangesFromContextDidSaveNotification:.
If I quit and relaunch the app, the updates were committed
For reference, both my main app and test app implement the above functionality but the test app shows the updates merged correctly and the main app does not.
I am looking for suggestions on what would cause mergeChangesFromContextDidSaveNotification: not to successfully merge and/or invalidate the updated object.
Thanks!
The fetched results controller does not receive the controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: delegate message. Instead it sends it to the delegate in response to a change in the context. If that delegate message is being sent and does contain the proper object, then the main context is aware of the changes.
If so, then your error is most likely in the the code interfacing the fetched results controller and the tableview. Something is preventing the object from appearing correctly.
The issue I had that caused mergeChangesFromContextDidSaveNotification not to work was I created a new NSPersistentStoreCoordinator for each new NSManagedObjectContext. When I shared NSPersistentStoreCoordinator between all MOCs, mergeChangesFromContextDidSaveNotification works perfectly. Also, make sure mergeChangesFromContextDidSaveNotification is called on the thread that owns the MOC.
From the research I did, it is safe to share NSPersistentStoreCoordinator between threads for use with NSManagedObjectContext. NSManagedObjectContext will lock the persistent store as necessary.
One possible answer: you have a third MOC you didn't know was there / forgot about. (This happened to me.)
I had
mainMOC
editorMOC
viewerMOC which came about by accidentally misguided subclassing - it was supposed to be looking at the main MOC, but instead was creating its own and looking at a frozen state. The "checking for edits" relationship was going the other direction, because it was expected to be the "editor" in this scenario.
the notification is correctly invoking the callback, and the data is being merged correctly into the main MOC (which i could tell because the data was correct on relaunch)
note: refreshObject:mergeChanges: is not needed. it's something i tried, too, when it wasn't working, but the merge of the notification should take care of all the objects for you.
- (void)twinStackUpdated:(NSNotification *)notification {
[[self managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
}
not sure that this was your problem, but it might be someones, so there it is.
In an effort to accept something even though I don't have a definitive solution, here's what I've done to address this and the problem seems to be resolved.
I believe this was a cache issue with NSFetchedResultsController. I've since simplified this code and made sure that each NSFetchedResultsController uses it's own cache. I've been able to remove the call to 'refreshObject' and things seem to be working correctly.
When updating the managedObjectContext is it ok practice to do the save setup in view controllers that may be released or should the appDelegate handle the saving of the managedObjectContext so that even if the viewController is released the save finishes?
I'm leaning towards the idea of moving the save step into my appDelegate and having viewControllers call [appDelegate saveContext]; when an update is made, though perhaps thats moot since the viewController won't finish releasing until its done saving to CD either way...?
For instance, is there any difference between these two actions, done from a subViewController:
[appDelegate.managedObjectContext save:&error]
and
[appDlegate saveContext]
Where there is a method in appDelegate that runs [managedObjectContext save:&error]
Thanks,
Sam
Guessing that your application is single threaded, you are guaranteed that the save will finish before the view controller is released because the thread will block on the save.
If you are running a multi-threaded application; then
You should not be saving on the background thread, that is dangerous in Core Data.
You should not be accessing the view controller on the background thread because all view related activity should be performed on the main thread.
Therefore, there is no "correct" situation where you would have to worry about a -save: not finishing no matter what object you call it from because it is a blocking call.
If you're just using one managedObjectContext through your app, it's not a bad idea to keep the save functionality in the app delegate, so that regardless of the state of your view controllers through the application's lifecycle, any updates will be saved when the app terminates.
That being said, I've found it useful to add additional save points, sometimes in view controllers, that will save the database after doing some significant updates. This way I've got my data saved even if the app crashes or the final save operation is otherwise prevented from completing.
I'm a relative newbie to Core Data so please inform me if this is bad practice.
I have a managed object context in a separate thread that is creating a few hundred managed objects, and when it saves, the did save notification is passed to the main thread and my other context (on the main thread) is updated:
In Thread
[ApplicationDelegate performSelectorOnMainThread:#selector(managedObjectContextDidSave:)
withObject:notification
waitUntilDone:NO];
The problem is that the merging is taking a very long time, sometimes 40-50 seconds, and this is locking up the main thread & UI. Is there any reason why it would take this long to update?
Edit
This appears to only happen if there is a fetched results controller that is currently displaying data that will be affected by the merging. Any ideas?
I am assuming that your NSFetchedResultsController has a NSFetchedResultsControllerDelegate which is having to handle lots of updates as a result of the merge. These updates will result in lots of activity within the table, which might be causing the lag.
What you might have to do is create a wrapper method which when called on the main thread temporarily removes the delegate from the NSFetchedResultsController, then invokes managedObjectContextDidSave, then refreshed the table, then reinstates the NSFetchedResultsControllerDelegate.
Core Data newbie speaking here.
In my application I have two NSManagedObjectContext that refer to that same NSPersistentStorageController.
One ManagedObjectContext (c1) is in the main thread --created when I create a NSFetchedResultsController -- and the second ManagedObjectContext (c2) created in a second thread, running in the background, detached from the main thread.
In the background thread I pull some data off a website and insert the entities created for the pulled data in the thread's ManagedObjectContext (c2).
In the meanwhile, the main thread sits doing nothing and displaying a UITableView whose data do be display should be provided by the NSFetchedResultsController.
When the background thread has finished pulling the data and inserting entities in c2, c2 saves, and the background thread notifies the main thread that the processing has finished before it exiting.
As a matter of fact, the entities that I have inserted in c2 are know by c1 because it can ask it about one particular entity with [c1 existingObjectWithID:ObjectID error:&error];
I would expect at this point, if I call on my tableview reloadData to see some rows showing up with the data I pulled from the web in the background thread thanks to the NSFetchedResults controller which should react to the modifications of its ManagedObjectContext (c1).
But nothing is happening! Only if I restart the application I see what I have previously pulled from the web!
Where am I doing things wrong?
Thank you in advance!
Reloading the table will not refetch items from the persistent store as you are expecting. Reloading the table is a "UI" level action and fetching data is at the model level. While this is fine for any changes that occur in the main NSManagedObjectContext, this will not cause the main NSManagedObjectContext to become aware of changes that occurred in another NSManagedObjectContext.
Depending on your design you need to do one of the following:
When the main thread receives the notification on the background thread's save it needs to call -mergeChangesFromContextDidSaveNotification:.
When this is complete you need to save the main NSManagedObjectContext
During the save it will trigger your NSFetchedResultsController with changes
The NSFetchedResultsController will notify its delegate (normally your view controller) of the changes and give you an opportunity to update the table view.
I suspect that you are not doing one of those items and thus breaking the chain of events.
Example
There is a solid example of multi-thread notifications in my article over on the MDN. If you are not familiar with the MDN, I highly suggest taking a look.