CoreData could not fulfill a fault for
Asked Answered
L

5

35

I have a really annoying problem, which I just can't seem to get fixed.

I have a view when I send a message that gets saved to the Core Data, when thats done it asked the database for a random message (sentence) and saved that as well to an other row in the database.

If I do the last part hardcoded, without fetching data from the DB, it works all fine and dandy, but as soon as I fetch the random row from the DB it goes crazy.

In my AppDelegate.m:

- (void)save {
    NSAssert(self.context != nil, @"Not initialized");
    NSError *error = nil;
    BOOL failed = [self.context hasChanges] && ![self.context save:&error];
    NSAssert1(!failed,@"Save failed %@",[error userInfo]);
}

- (NSString*)selectRandomSentence
{
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Sentences" inManagedObjectContext:self.managedObjectContext];
    [request setEntity:entity];

    NSError *error = nil;
    NSUInteger count = [self.context countForFetchRequest:request error:&error];

    NSUInteger offset = count - (arc4random() % count);
    [request setFetchOffset:offset];
    [request setFetchLimit:1];

    NSArray *sentenceArray = [self.context executeFetchRequest:request error:&error];

    [request release];

    return [[sentenceArray objectAtIndex:0] sentence];
}

- (NSManagedObjectContext *)context {

    if (_managedObjectContext != nil)
        return _managedObjectContext;

    NSPersistentStoreCoordinator *coordinator = [self coordinator];
    if (coordinator != nil) {
        _managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }

    return _managedObjectContext;
}

In my ChatController.m:

- (void)didRecieveMessage:(NSString *)message
{
    [self addMessage:message fromMe:NO];
}

#pragma mark -
#pragma mark SendControllerDelegate

- (void)didSendMessage:(NSString*)text {
    [self addMessage:text fromMe:YES];
}

#pragma mark -
#pragma mark Private methods

- (void)responseReceived:(NSString*)response {
    [self addMessage:response fromMe:NO];
}

- (void)addMessage:(NSString*)text fromMe:(BOOL)fromMe {
    NSAssert(self.repository != nil, @"Not initialized");
    Message *msg = [self.repository messageForBuddy:self.buddy];
    msg.text = text;
    msg.fromMe = fromMe;

    if (fromMe)
    {
        [self.bot talkWithBot:text];
    }

    [self.repository asyncSave];

    [self.tableView reloadData];
    [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:[self.buddy.messages count] - 1] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}

In My OfflineBot.m:

- (void)talkWithBot:(NSString *)textFromMe
{
    AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
    [self didRecieveMessage:[delegate selectRandomSentence]];
}

- (void)didRecieveMessage:(NSString *)message
{
    if ([self.delegate respondsToSelector:@selector(didRecieveMessage:)])
        [self.delegate didRecieveMessage:message];
}

Repository.m

- (Message*)messageForBuddy:(Buddy*)buddy {
    Message *msg = [self.delegate entityForName:@"Message"];
    msg.source = buddy;
    [self.delegate.managedObjectContext refreshObject:buddy mergeChanges:YES];
    return msg;
}

- (void)asyncSave {
    [self.delegate save];
}

The error:

2012-08-10 00:28:20.526 Chat[13170:c07] * Assertion failure in -[AppDelegate save], /Users/paulp/Desktop/TestTask/Classes/AppDelegate.m:28 2012-08-10 00:28:20.527 Chat[13170:c07] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Save failed {type = immutable dict, count = 2, entries => 1 : {contents = "NSAffectedObjectsErrorKey"} = ( " (entity: Sentences; id: 0x6b8bf10 ; data: )" ) 2 : {contents = "NSUnderlyingException"} = CoreData could not fulfill a fault for '0x6b8bf10 ' }

What am I doing wrong?

Update I pinpointed the error to this row:

NSArray *sentenceArray = [self.context executeFetchRequest:request error:&error];

When I execute that row, I get the error... that is when fetching the data. The error, however, seems to turn up when saving the new data to the Messages entity. The random sentence is fetched from Sentences.

After I changed the asyncSave method to saving directly (thus not using a new thread) it saves the first chat, but nothing after that. It dies.

Update It all seems to work using this in my didFinishLaunchingWithOptions:

[self.context setRetainsRegisteredObjects:YES];

I understand that hereby the CodeData Object Model Context doesn't release its objects, which seems to be the problem betweens adding and saving. But why?

Lexington answered 9/8, 2012 at 22:29 Comment(14)
Paul, in your update you mention that you pinpointed your error to executeFetchRequest call yet your error occurs while saving your context which doesn't make sense. Any chance you use multiple threads in your application? That is very often culprit for NSInternalInconsistencyExceptionClarkson
What's the difference between self.context and self.managedObjectContext. You seem to be using them interchangeably, and that does not sound like a good idea.Deepseated
Also, it appears you are not using ARC, but it does not look like you are properly managing your memory. This error usually means you deleted something from the store, but didn't properly update your MOC, or you've got multiple threads messing with the same MOC.Deepseated
Self.context is nothing more then a method in my AppDelegate. I have analyzed and checked my code, but I have no memory leaks there. How can I ensure I am on the same thread?Lexington
... I updated the question. Also, to explain my last comment: I do not have any leaks at all. Neither do I get any analizy or warnings when building.Lexington
what @PeterPajchl said: is your application multi-threaded? Are you accessing your Core Data managed object context from multiple threads? (Uh oh!)Alan
I was before, but I removed that part. The asyncsave did a preformselector with delay 0, which delayed the save into a new thread. As far as I know and can see, I am not using threads.Lexington
also, instead of arc4random() % count, you can use arc4random_uniform( count ). It's more correct, not that it matters.Alan
Before your access add this: assert( [ NSThread currentThread ] == [ NSThread mainThread ] )Alan
.. Ok, I should do that both when reading and saving? If it is not the same it will cast an exception in the log... am I right?Lexington
also, you have an OBOB (off by one bug) I believe: (count - arc4random() % count) can be one to high. Use count - 1 - arc4random() % countAlan
yes--not on main thread will crash... You can use NSAssert() to throw an exception instead..Alan
Ok. I will give it a try in a bit. If I am not on the main thread, how can I ensure I will be? Of course a part from removing preformeSelector ect.Lexington
let us continue this discussion in chatAlan
W
6

It is not really conceptually possible to "save" Core Data objects "in different rows". Remember, Core Data is an object graph and not a database.

If you want to "relocate" your sentence, the best way is to destroy it and recreate it. If you want to keep the old instance, just create a new one and then fill in the properties from the existing one.

For destroying, use

[self.context deleteObject:sentenceObject];

For recreating, use

Sentence *newSentence = [NSEntityDescription insertNewObjectForEntityForName:
  "Sentences" inManagedObjectContext:self.context];
newSentence.sentence = sentenceObject.sentence;
// fill in other properties, then
[self.context save:error];

If you would like to read up on this, look at "Copying and Copy and Paste" in the "Using Managed Objects" section of the "Core Data Programming Guide".

Weissmann answered 13/8, 2012 at 13:12 Comment(0)
D
66

Hmm. Are you properly implementing concurrency following this guide? The problem you are seeing is a common one when using core data across multiple threads. Object was deleted in your "background context", while then it's being accessed by another context. Calling [context processPendingChanges] on your background context after the delete but before the save may help.

There is also a WWDC 2010 session (137) on optimizing core data performance that goes into deletes a bit.

When you execute a fetch Core Data returns a collection of objects matching the predicate you supplied. Those objects don't actually have their property values set yet. It's when you access a property that Core Data goes back to the store to "fire the fault" - populate the property with data from the store. "Could not fulfill a fault..." exceptions happen when Core Data goes to the store to get the property values for an object, but the object does not exist in the persistent store. The managed object context thought it should exist, which is why it could attempt the fault - which is where the problem is. The context that caused the exception to be thrown did not know that this object had been deleted from the store by something else (like another context).

Note the the above Concurrency Guide is now out of date, you should be using parent-child contexts and private queue concurrency rather than the older thread confinement model. Parent-child contexts are much less likely to run into "Could not fulfill a fault..." for many reasons. And please, file a documentation bug or use the feedback form to request that the concurrency guide be updated.

Deliberation answered 15/8, 2012 at 23:7 Comment(3)
+1 I think this answer actually addresses the technical issue in question in the OP's problemSnorkel
re the link to the guide: "Important: Best practices for concurrency with Core Data have changed dramatically since this document was written; please note that this chapter does not represent current recommendations."Sura
@Deliberation - oh! sorry about that! If you don't object, I'll delete my comment(s) in the interest of keeping this joint tidy (and not making me look quite as daft as I actually am).Sura
W
6

It is not really conceptually possible to "save" Core Data objects "in different rows". Remember, Core Data is an object graph and not a database.

If you want to "relocate" your sentence, the best way is to destroy it and recreate it. If you want to keep the old instance, just create a new one and then fill in the properties from the existing one.

For destroying, use

[self.context deleteObject:sentenceObject];

For recreating, use

Sentence *newSentence = [NSEntityDescription insertNewObjectForEntityForName:
  "Sentences" inManagedObjectContext:self.context];
newSentence.sentence = sentenceObject.sentence;
// fill in other properties, then
[self.context save:error];

If you would like to read up on this, look at "Copying and Copy and Paste" in the "Using Managed Objects" section of the "Core Data Programming Guide".

Weissmann answered 13/8, 2012 at 13:12 Comment(0)
D
1

check the core data mechanism. "Faulting reduces the amount of memory your application consumes. A fault is a placeholder object that represents a managed object that has not yet been fully realized, or a collection object that represents a relationship:"

Doubletongue answered 30/10, 2012 at 3:39 Comment(3)
in Core Data Programming Guide,, and search with "CoreData could not fulfill a fault" in the document, the unit "Fault mechanism"Doubletongue
"Firing a fault" means Core Data has to go to the persistent store to populate a managed object property value. Accessing a property that is a fault will do this. "Could not fufil a fault" means it went to the store, but there was no data for this object - usually because something deleted the record.Deliberation
Thank you for this, i sometimes see this fault and didn't understood why. This happens when i log the whole object but i still can access individual fields.Grane
S
0

This happens because you are adding the "random message" to your new row before completion of fetching all your relations of the first call.

You can add a prefetch to your 1st call to avoid the lazy loading and the issue will be solved, I believe.

This is how we can do prefetching for the request:

[request setRelationshipKeyPathsForPrefetching:[NSArray arrayWithObjects:@"whatEverOfYourWillNumberOne",@"whatEverOfYourWillNumberTwo", nil]];

Hope that helps.

Salba answered 25/2, 2013 at 4:23 Comment(0)
E
0

I fixed the error with changing NSFetchedResultsController's "cacheName" string to nil.

NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"Root"nil];

Epicurean answered 2/4, 2014 at 8:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.