1
votes

My application has NSOperation subclasses that fetch and operate on managed objects. My application also periodically purges rows from the database, which can result in the following race condition:

  • An background operation fetches a bunch of objects (from a thread-specific context). It will iterate over these objects and do something with their properties.
  • A bunch of rows are deleted in the main managed object context.
  • The background operation accesses a property on an object that was deleted from the main context. This results in an 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault'

Ideally, the objects that are fetched by the NSOperation can be operated on even if one is deleted in the main context. The best way I can think to achieve this is either to:

  • Call [request setReturnsObjectsAsFaults:NO] to ensure that Core Data won't try to fulfill a fault for an object that no longer exists in the main context. The problem here is I may need to access the object's relationships, which (to my understanding) will still be faulted.
  • Iterate through the managed objects up front and copy the properties I will need into separate non-managed objects. The problem here is that (I think) I will need to synchronize/lock this part, in case an object is deleted in the main context before I can finish copying.

Am I missing something obvious? It doesn't seem like what I'm trying to accomplish is too out of the ordinary. Thanks for your help.

3

3 Answers

0
votes

You said each thread has its own context. That's good. However, they also need to stay synchronized with changes to each other (how depends on their hierarchy).

Are the all assigned to the same persistent store coordinator, or do they have parent/child relationships?

Siblings should monitor NSManagedObjectContextObjectsDidChangeNotification from other siblings. Parents will automatically get notified when a child context saves.

0
votes

I ended up mitigating this by perform both fetches and deletes on the same queue.

0
votes

Great question, I can only provide a partial answer and would really like to know this too. Unfortunately your own solution is more of a workaround but not really an answer. What if the background operation is very long and you can't resort to running it on the main thread?

One thing I can say is that you don't have to call [request setReturnsObjectsAsFaults:NO] since the fetch request will load the data into the row cache and will not go back to the database when a fault fires for one of the fetched objects (see Apples documentation for NSFetchRequest). This doesn't help with relationships though.

I've tried the following:

  1. On NSManagedObjectContextWillSave notification, wait for the current background task to finish and prevent new tasks from starting with something like

    -(void)contextWillSave:(NSNotification *)notification {
        dispatch_sync(self.backgroundQueue, ^{
            self.suspendBackgroundOperation = YES;
        });
    }
    
  2. Unset suspendBackgroundOperation on NSManagedObjectContextDidSave notification

However the dispatch_sync call introduces possible dead locks so this doesn't really work either (see my related question). Plus it would still block the main thread until a potentially lengthy background operation finishes.