RyanJM
RyanJM

Reputation: 7068

CoreData could not fulfill a fault when using mergeChangesFromContextDidSaveNotification

I'm getting uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x9f481f0 <x-coredata://ABF084FE-4BF3-4FC3-918A-BFF043589B8A/Structure/p21316>' at this line in MagicalRecord:

[[self MR_defaultContext] mergeChangesFromContextDidSaveNotification:notification];

I've been debugging this for 2 days and am still unsure what is happening.

I've narrowed it down to the part of my import code which deletes every object (of a given entity) which wasn't just imported (therefore removing old objects). It looks like this:

- (void)deleteNonImportedEntities
{
    NSString *entityName = [self.configuration entityName];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (myID IN %@)", self.resourceIds];

    [MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) {
        [localContext MR_setWorkingName:@"deleteNonImportedEntities context"];
        [NSClassFromString(entityName) MR_deleteAllMatchingPredicate:predicate inContext:localContext];
    }];
}

Pretty stright forward. The problem with this error is it only happens 1 out of 10 or so times. And if I put too much logging in here then it happens even less. But I've been working on narrowing it down. I changed the saveWithBlockAndWait method to show me how many objects are inserted (i), updated (u), or deleted (d) for that context.

+ (void) saveWithBlockAndWait:(void(^)(NSManagedObjectContext *localContext))block;
{
    NSManagedObjectContext *savingContext  = [NSManagedObjectContext MR_rootSavingContext];
    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextWithParent:savingContext];

    [localContext performBlockAndWait:^{
        [localContext MR_setWorkingName:NSStringFromSelector(_cmd)];

        if (block) {
            block(localContext);
        }
        MRLogVerbose(@"Saving saveWithBlockAndWait. (i: %i, u: %i, d: %d)", [[localContext insertedObjects] count], [[localContext updatedObjects] count], [[localContext deletedObjects] count]);
        [localContext MR_saveWithOptions:MRSaveParentContexts|MRSaveSynchronously completion:nil];
        MRLogVerbose(@"Finished saveWithBlockAndWait");
    }];
}

I also changed rootContextDidSave (since that is where the exception is happening) so it would give the information from the notification that should be sent from the above save (once it goes up the the root saving context).

+ (void) rootContextDidSave:(NSNotification *)notification
{
    if ([notification object] != [self MR_rootSavingContext])
    {
        return;
    }

    if ([NSThread isMainThread] == NO)
    {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self rootContextDidSave:notification];
        });

        return;
    }

    int inserted = [[[notification userInfo] objectForKey:@"inserted"] count];
    int updated = [[[notification userInfo] objectForKey:@"updated"] count];
    int deleted = [[[notification userInfo] objectForKey:@"deleted"] count];
    MRLogVerbose(@"Merging changes from notification to the default context (NSManagedObjectContext+MagicalRecord.m) (i: %i, u: %i, d: %i)", inserted, updated, deleted);
    [[self MR_defaultContext] mergeChangesFromContextDidSaveNotification:notification];
}

When the code doesn't crash it the log looks like this:

... [278:21433] Saving saveWithBlockAndWait. (i: 0, u: 0, d: 9)
... [278:21433] → Saving <NSManagedObjectContext (0x191f8b30): deleteNonImportedEntities context> on a background thread
... [278:21433] → Save Parents? YES
... [278:21433] → Save Synchronously? YES
... [278:21433] → Saving <NSManagedObjectContext (0x1558d6e0): MagicalRecord Root Saving Context> on a background thread
... [278:21433] → Save Parents? YES
... [278:21433] → Save Synchronously? YES
... [278:21187] Merging changes from notification to the default context (NSManagedObjectContext+MagicalRecord.m) (i: 0, u: 13, d: 9)
... [278:21433] → Finished saving: <NSManagedObjectContext (0x1558d6e0): MagicalRecord Root Saving Context> on a background thread
... [278:21433] Finished saveWithBlockAndWait

I'm not sure why it would be that the update number gets increased as it changes context.

The following is when it terminated:

... [284:22234] Saving saveWithBlockAndWait. (i: 0, u: 0, d: 8)
... [284:22234] → Saving <NSManagedObjectContext (0xa062210): deleteNonImportedEntities context> on a background thread
... [284:22234] → Save Parents? YES
... [284:22234] → Save Synchronously? YES
... [284:22234] → Saving <NSManagedObjectContext (0x17df7b60): MagicalRecord Root Saving Context> on a background thread
... [284:22234] → Save Parents? YES
... [284:22234] → Save Synchronously? YES
... [284:22234] → Finished saving: <NSManagedObjectContext (0x17df7b60): MagicalRecord Root Saving Context> on a background thread
... [284:22234] Finished saveWithBlockAndWait
All Exceptions - Breakpoint
... [284:22200] *** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x9f481f0 <x-coredata://ABF084FE-4BF3-4FC3-918A-BFF043589B8A/Structure/p21316>''
*** First throw call stack:
(0x2ae8a49f 0x389bec8b 0x2aba47dd 0x2aba3bd1 0x2aba3a35 0x2abb261d 0x2bb118c9 0x2bb1148b 0x2bb11249 0x2bb11001 0x2abb8ac5 0x2abb7cc1 0x2ac8b103 0x2ac187ad 0x2ac1894f 0x2abb7b5d 0x2ae42c61 0x2ad9e6d5 0x2bad0189 0x2abb7acf 0x2ac18433 0x2ac1864d 0x9fca3 0x9fcfb 0xc1f9db 0xc1f9c7 0xc233ed 0x2ae503b1 0x2ae4eab1 0x2ad9c3c1 0x2ad9c1d3 0x321310a9 0x2e3abfa1 0x7e821 0x38f3eaaf)
libc++abi.dylib: terminating with uncaught exception of type _NSCoreDataException

How do I properly debug this issue? It seems like the import which happens just before the delete method is called works fine and that gets merged into the default context. Why would what is being inserted/updated/deleted change as the notification is sent?

I don't have any views which are watching for this data to be changed. And I've checked the other NSFetchedResultsControllers I have and I don't believe they are being fired (I've got log statements there too).

Update: looking at the notification as it gets sent up the chain. When the method is first called (and it isn't in the main thread) it has just a few deleted and updated. But when it gets to the main thread it is just a huge list of inserted ones. And the ones that were deleted in the first notification are faulted ones in the second. Now just to figure out why they are different.

Upvotes: 0

Views: 1272

Answers (2)

Cullen SUN
Cullen SUN

Reputation: 3547

enter image description here

I am also using MagicalRecord, and recently had come to the similar problem.

CoreData: warning: An NSManagedObjectContext delegate overrode fault handling behavior to silently delete the object with ID '0xd0000000071c0006 < x-coredata://F5A1A83A-F160-475D-B313-EC68CDADCEF8/MyModel/p455 >' and substitute nil/0 for all property values instead of throwing.

My Core Data stack is a bit complicated, the root is the NSPersistentStoreCoordinator. The right side is my main nested stack for the Application.

  • MR_rootSavingContext, background context, for the writing to disk
  • MR_defaultContext, for the main thread UI, e.g. NSFetchedResultsController
  • MyWorkerContext, background context, for the Core Data's entry point of writing from API logics

BackgroundContext1 or BackgroundContext2 is my plan for implementing some massive data operations that shall round on background threads.

Please note the red line showing BackgroundContext1 listens to MyWorkerContext and merges changes, this will cause the error. Reason is when BackgroundContext1 saves and posts notifications about the changes, changes have not been propagated to the disk yet. No data in place for BackgroundContext1, so the Faulting cannot be fulfilled.

My suggestion is that NSManagedObjectContext shall only merge changes from other NSManagedObjectContexts of the same level or parents. E.g. the following two cases shall be ok:

  • BackgroundContext1 listen and merge changes from MR_rootSavingContext
  • BackgroundContext1 listen and merge changes from MyWorkerContext

Upvotes: 0

RyanJM
RyanJM

Reputation: 7068

I have diagnosed the issue and it has to do with a race condition.

Little background on MagicalRecord first. It uses a "Saving" context which all other contexts use as the parent. The "Default" context is the one used for the UI and therefore is the one that is listening for the notification so it can merge the changes in. When you create another context (or use MagicalRecord's save block) it sets the Saving context as the parent.

The issue was that I have two main methods in my import class. One to import all of the objects based on the JSON data (which has been saved to a file) and one to delete objects which weren't just imported. Here is the basic concept of each:

- (void)importEntities
{
    ...
    [saveResources readJSONResponsesFromDisk:^(NSDictionary *response) {
        [MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) {
            NSArray *resources = [response objectForKey:JSONkey];

            NSArray *array = [NSClassFromString(entityName) MR_importFromArray:resources inContext:localContext];

            [weakSelf.resourceIds addObjectsFromArray:[array valueForKey:@"myID"]];
        }];
    }];
}

- (void)deleteNonImportedEntities
{
    ...
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (myID IN %@)", self.resourceIds];

    [MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) {
        [NSClassFromString(entityName) MR_deleteAllMatchingPredicate:predicate inContext:localContext];
    }];
}

Now my original purpose of using saveWithBlockAndWait each time was because of memory issues. Each file that it loops over could have 500 entities in it. And those could each create multiple other objects due to the way that MagicalRecord handles imports. Therefore I want to flush the data from memory (and save to disk) as often as possible so that it doesn't build up too much.

Before getting to the core of the issue one needs to understand the way data is saved in CoreData. Since the context setup by saveWithBlockAndWait has a parent of the Saving context, when it gets saved, its changes are automatically merged into the Saving context. Then the Saving saving context sends its NSManagedObjectContextDidSaveNotification (the child context sent its too but the default context ignores it). But, before the default context merges the change sent from the saving context the deleteNonImportedEntities is run and saved. Thus it gets saved to the Saving context before the default context (1 out of ~15 times). Therefore when the default context does try to merge those changes, some of the objects the notification points to are not faulted (they've been deleted in the Saving context). Therefore it fails.

Now that I know that, I've taken out the saveWithBlockAndWait calls and use just one localContext for the entire class. Guess I'll have to go back to using an autorelease pool and saving periodically.

Update: Actually, that results in the same problem. So I'm trying to figure out another solution to it.

Update 2: I ended up taking Aaron's suggestion below and moving it to it's own operation. I also changed the priority levels to help separate them a little more.

Upvotes: 2

Related Questions