John Gallagher
John Gallagher

Reputation: 6278

Core Data: Merging children added on a background context is funky

Background

Data Model

Problem

Question

Is what I'm trying to do impossible? I can see how core data might struggle to do the merges successfully. Or is there an issue with my code?

Code

JGGroupController

 -(id)init {
     self = [super init];
     queue = [[NSOperationQueue alloc] init];
     [queue setMaxConcurrentOperationCount:10]; // If this is 1, it works like a dream. Anything higher and it bombs.
     return self;
 }

 -(IBAction)addTrainingEntryChild:(id)sender {
     moc  = [[NSApp delegate] managedObjectContext];
     JGTrainingBase *groupToAddTo = [[tree selectedObjects] objectAtIndex:0];
     for (NSUInteger i = 0; i < 20; i++) {
         JGAddChildrenObjectOperation    *addOperation = [[JGAddChildrenObjectOperation alloc] init]; 
         [addOperation addChildObjectToGroup:[groupToAddTo objectID]];
         [queue addOperation:addOperation];
     }
 }

JGAddChildrenObjectOperation - NSOperation subclass

 -(id)addChildObjectToGroup:(NSManagedObjectID *)groupToAddToID_ {
     groupToAddToObjectID = groupToAddToID_;
     return self;
 }

 -(void)main {
     [self startOperation];
     JGTrainingBase *groupToAddTo    = (JGTrainingBase *)[imoc objectWithID:groupToAddToObjectID];
     JGTrainingBase *entryChildToAdd = [JGTrainingBase insertInManagedObjectContext:imoc];
     [groupToAddTo addChildren:[NSSet setWithObject:entryChildToAdd]];
     [imoc save];
 [self cleanup];
     [self finishOperation];
 }

 -(void)mergeChanges:(NSNotification *)notification {
     NSManagedObjectContext *mainContext = [[NSApp delegate] managedObjectContext];
     [mainContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
                                   withObject:notification
                                waitUntilDone:YES];  
 }


 -(void)startOperation {
            // Omitted - Manage isExecuting, isPaused, isFinished etc flags

     imoc = [[NSManagedObjectContext alloc] init];
     [imoc setPersistentStoreCoordinator:[[NSApp delegate] persistentStoreCoordinator]];
     [imoc setUndoManager:nil];
     [imoc setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
     [imoc setStalenessInterval:0];

     [[NSNotificationCenter defaultCenter] addObserver:self
                                              selector:@selector(mergeChanges:) 
                                                  name:NSManagedObjectContextDidSaveNotification 
                                                object:imoc];
 }

 -(void)finishOperation {
            // Omitted - Manage isExecuting, isPaused, isFinished etc flags
 }

Upvotes: 2

Views: 763

Answers (1)

ImHuntingWabbits
ImHuntingWabbits

Reputation: 3827

Your operations are using different "versions" of the entity from the store. Consider this order of operations:

You create 2 operations, let's call them O:F and O:G which are to add children F and G to group 1, noted as G:1 with a children entry set [A,B,C,D,E].

The operation queue dequeues O:F and O:G at the same time, thus they both fetch a managed object context and entity G:1.

O:F sets children of G:1 to [A,B,C,D,E,F]. O:G sets children of G:2 to [A,B,C,D,E,G].

It doesn't matter which operation wins, you will end up with either [A,B,C,D,E,F] or [A,B,C,D,E,G], both of which are incorrect values in the store.

I believe CoreData should be throwing an optimistic locking error in one of those threads though, as it's changes would be out of date. But I could be wrong.

The bottom line is you're mutating the same object across threads without synchronizing the state of the object. Instead of creating 20 operations create 1 operation which adds 20 objects, but you have a core architectural problem of trying to mutate the same object from multiple threads without synchronization.

That will fail every time.

Upvotes: 1

Related Questions