Tom Redman
Tom Redman

Reputation: 5650

NSInternalInconsistencyException when saving Core Data on Background Context

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

Answers (2)

Jody Hagins
Jody Hagins

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

Tom Redman
Tom Redman

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

Related Questions