Reputation: 5650
I've read everything on SO regarding this error, and still can't nail down why it's happening in my app.
I get the following error when saving several Core Data objects using a background context:
*** Terminating app due to uncaught exception "NSInternalInconsistencyException", reason: "Failed to process pending changes before save. The context is still dirty after 100 attempts. Typically this recursive dirtying is caused by a bad validation method, -willSave, or notification handler.
In the code below, ArticleManager
's addArticle
is called in a loop on the main thread. There may be 0-200+ articles to add. This error generally occurs between article count 100-150.
//ArticleManager.m
-(id)init
{
... //normal init stuff
dispatch_queue_t request_queue = dispatch_queue_create("com.app.articleRequest", NULL);
}
-(void) addArticle:(Article *)article withURLKey:(NSString *)url
{
//check if exists
if ([downloadedArticles objectForKey:url] == nil && article != nil)
{
//add locally
[downloadedArticles setObject:article forKey:url];
//save to core data
SaveArticle *saveArticle = [[SaveArticle alloc] init];
[saveArticle saveArticle:article withURL:url onQueue:request_queue];
}
}
//SaveArticle.m
@implementation SaveArticle
@synthesize managedObjectContext;
@synthesize backgroundContext;
-(id)init
{
if (![super init]) return nil;
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
managedObjectContext = [appDelegate managedObjectContext];
backgroundContext = [[NSManagedObjectContext alloc] init];
[backgroundContext setPersistentStoreCoordinator:[managedObjectContext persistentStoreCoordinator]];
return self;
}
-(void)saveArticle:(Article *)article withURL:(NSString *)url onQueue:(dispatch_queue_t)queue
{
//save persistently in the background
dispatch_async(queue, ^{
ArticleCache *articleCacheObjectModel = (ArticleCache *)[NSEntityDescription insertNewObjectForEntityForName:@"ArticleCache" inManagedObjectContext:backgroundContext];
if (article != nil)
{
[articleCacheObjectModel setArticleHTML:article.articleHTML];
[articleCacheObjectModel setUrl:url];
NSError *error;
//Save the background context and handle the save notification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:backgroundContext];
if(![backgroundContext save:&error]) //ERROR OCCURS HERE, after many saves
{
//This is a serious error saying the record
//could not be saved. Advise the user to
//try again or restart the application.
}
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSManagedObjectContextDidSaveNotification
object:backgroundContext];
}
});
}
/* Save notification handler for the background context */
- (void)backgroundContextDidSave:(NSNotification *)notification {
/* Make sure we're on the main thread when updating the main context */
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(backgroundContextDidSave:)
withObject:notification
waitUntilDone:NO];
return;
}
/* merge in the changes to the main context */
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
@end
Upvotes: 1
Views: 2675
Reputation: 28349
Yep, if you use the confinement concurrency model (which is what you get with init), then you must guarantee that you only use the MOC in the thread in which it was created.
You can create a MOC with NSPrivateQueueConcurrencyType, and then just use
[moc performBlock:^{
}];
to perform operations. It has its own internal queue, and will run all requests in the background, synchronizing access with other calls.
You can use NSMainQueueConcurrencyType to tie a MOC to run only on the main thread.
Upvotes: 0
Reputation: 5650
OK, so reading the official documentation is somewhat useful.
From Apple (emphasis mine):
Concurrency
Core Data uses thread (or serialized queue) confinement to protect managed objects and managed object contexts (see “Concurrency with Core Data”). A consequence of this is that a context assumes the default owner is the thread or queue that allocated it—this is determined by the thread that calls its init method. You should not, therefore, initialize a context on one thread then pass it to a different thread. Instead, you should pass a reference to a persistent store coordinator and have the receiving thread/queue create a new context derived from that. If you use NSOperation, you must create the context in main (for a serial queue) or start (for a concurrent queue).
So my problem was that I was initialized the background context on the main thread, but then used Grand Central Dispatch via dispatch_async
which performs the save on a background thread (using the context that was created on the main thread).
I fixed it by adding the context initialization to the background block:
-(void)saveArticle:(Article *)article withURL:(NSString *)url onQueue:(dispatch_queue_t)queue
{
//save persistently in the background
dispatch_async(queue, ^{
NSManagedObjectContext *backgroundContext = [[NSManagedObjectContext alloc] init];
[backgroundContext setPersistentStoreCoordinator:[managedObjectContext persistentStoreCoordinator]];
ArticleCache *articleCacheObjectModel = (ArticleCache *)[NSEntityDescription insertNewObjectForEntityForName:@"ArticleCache" inManagedObjectContext:backgroundContext];
if (article != nil)
{
[articleCacheObjectModel setArticleHTML:article.articleHTML];
[articleCacheObjectModel setUrl:url];
NSError *error;
//Save the background context and handle the save notification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:backgroundContext];
if(![backgroundContext save:&error])
{
//This is a serious error saying the record
//could not be saved. Advise the user to
//try again or restart the application.
}
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSManagedObjectContextDidSaveNotification
object:backgroundContext];
}
});
}
Upvotes: 1