1
votes

I have set up a nonhierarchical dual-MOC architecture (one for the main thread, one for a private thread) with save notifications for merging changes:

- (NSManagedObjectContext *)managedObjectContext
{
   if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        NSManagedObjectContext* mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        [mainContext setPersistentStoreCoordinator:coordinator];
        [mainContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];

        _managedObjectContext = mainContext;

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(contextDidSaveMainQueueContext:)
                                                     name:NSManagedObjectContextDidSaveNotification
                                                   object:_managedObjectContext];
    }
    return _managedObjectContext;
}

- (NSManagedObjectContext *)privateManagedObjectContext
{
    if (_privateManagedObjectContext != nil) {
        return _privateManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {

        NSManagedObjectContext* privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [privateContext setPersistentStoreCoordinator:coordinator];
        [privateContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];

        _privateManagedObjectContext = privateContext;

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(contextDidSavePrivateQueueContext:)
                                                     name:NSManagedObjectContextDidSaveNotification
                                                   object:_privateManagedObjectContext];
    }
    return _privateManagedObjectContext;

}

- (void)contextDidSavePrivateQueueContext:(NSNotification *)notification
{
    @synchronized(self) {
        [self.managedObjectContext performBlock:^{
            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

- (void)contextDidSaveMainQueueContext:(NSNotification *)notification
{
    @synchronized(self) {
        [self.privateManagedObjectContext performBlock:^{
           [self.privateManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];          
        }];
    }
}


Now, I've updated some object A on the main thread in the main MOC, and now I'm in some method and working within a block on my private MOC queue/thread:

[self.privateManagedObjectContext performBlockAndWait:^{
   ...

I save my main-thread MOC:

[self.managedObjectContext performBlockAndWait:^{
    NSError *contextError;
    if (![self.managedObjectContext save:&contextError]) NSLog(@"ERROR SAVING MOC : %@ ; %@", contextError, [contextError userInfo]);
}];

The save is successful (verified) and triggers the save notification (verified), which executes the merger.

But the MOCs remain inconsistent: when I then fetch object A from my main MOC and log the number of objects in its relationship R, I find that the number of objects differs from when I fetch object A from my private MOC and log the number of objects in relationship R.

A bit downstream of this, in a related chain of events, I go to save my private MOC. The app pauses (only if I have enabled an all-exceptions or all-objective-c-exceptions breakpoint), and execution can be resumed without any apparent harm. This issue is described here: core data MOC save pauses execution and fails to save (w/o error or crash). I have a feeling it's related, but I suspect this failure to properly merge is a more fundamental problem.

Other notes:

  • I have tried every merge policy
  • I found that if I attempt to execute the merger using performBlockAndWait, the app hangs indefinitely; I don't know whether that is the expected behavior.
  • I've read every question I can find about this and tried everything I can think of.

Is there something wrong with this code? What else can I try? Thanks!

2
There is a possible race condition in your implementation. if privateManagedObjectContext is called from more than one thread in parallel (same goes for you main MOC). Also, there is no need for your '@synchronized(self)'. make sure the main and private MOCs are not initialised twice - Dan Shelly
@DanShelly Thanks for the tips. I removed @synchronized. I set a breakpoint and verified that the MOCs are initialized only once. If you think a race condition is likely coming into play here, I'd be happy for some elaboration, but it seems unlikely to me because I am testing this in a reasonably isolated fashion. No other methods/threads should be accessing core data during the execution of the method I'm troubleshooting, and the method always uses performBlockAndWait, as do all fetch requests. - mkc842
Consider this situation: your view controller asks for the main context and also a BG operation is taking place asking for the private context. during their initialisation they both need a coordinator, assuming that you implemented lazy loading, the persistentStoreCoordinator is called from 2 different threads in parallel. this will cause each context to use a different coordinator or might even crash your application if the context does not strongly keep the coordinator. this might not be directly causing your merge issues, but its something you might want to take into account. - Dan Shelly
Having multiple NSPersistentStoreCoordinator instances will not cause a merge problem or the problems that the OP is describing. SQLite and Core Data are designed for multiple instances of NSPersistentStoreCoordinator to be running in parallel. - Marcus S. Zarra
@MarcusS.Zarra multiple coordinators might not cause the issue described, however multiple "private context"s will (the merge is done directly to the currently assigned "private context"). as I mentioned, this is only one of the thing that are unsafe by using the code given by the OP, but not necessarily causing the problem. - Dan Shelly

2 Answers

1
votes

What does your merge code look like? If you are getting an indefinite block with a -performBlockAndWait during the merge then that means you were already on the thread for that context when the notification was received. It would help to see your code though.

Also seeing your code for the observer construction for the notification would help.

Why are you not doing a parent/child construct here when you are doing a main thread and private thread contexts?

As for the risk of multiple NSPersistentStoreCoordinator instances being initialized, that is not a risk. Even if you built 100 PSC instances it would still work just fine against a SQLite file. SQLite is designed for multi-user access. I do not see how that can be your issue.

Update

Ok, I don't recommend merging changes from main back to private. Your private queues should be used once and thrown away, even without a parent/child design. Merging in both directions can get messy very quickly and I suspect you are getting the same data merged back and forth. Putting in some logging will confirm that.

I would also suggest using parent/child. This type of situation is what it is designed for and will dramatically improve your merge performance. It is a fairly small code change (set PSC in the private, turn off observers, good to go) to test and verify the performance change.

1
votes

Not a direct answer but ...

Take a look at THIS sample project.

(Hope all breakpoint are left in place)

By suspending and resuming threads, a possible output could be:

2014-04-11 20:50:16.199 RaceCondition[13787:60b] setting timestamp: 2014-04-11 17:50:16 +0000
2014-04-11 20:50:16.202 RaceCondition[13787:1303] thread 0x8f81f00
2014-04-11 20:50:16.202 RaceCondition[13787:60b] main context: <NSManagedObjectContext: 0x8d5c170> created
2014-04-11 20:50:16.202 RaceCondition[13787:1303] no private context found
2014-04-11 20:50:16.203 RaceCondition[13787:60b] main context: <NSManagedObjectContext: 0x8d5c170> already exists
2014-04-11 20:50:28.122 RaceCondition[13787:60b] thread 0x8e48cd0
2014-04-11 20:50:28.122 RaceCondition[13787:60b] no private context found
** 2014-04-11 20:50:46.866 RaceCondition[13787:1303] private context: <NSManagedObjectContext: 0x8c59b60> set
2014-04-11 20:50:46.867 RaceCondition[13787:1303] working with context: <NSManagedObjectContext: 0x8c59b60>
2014-04-11 20:50:46.868 RaceCondition[13787:1303] event timestamp in private context: 2014-04-11 17:50:16 +0000
** 2014-04-11 20:51:22.923 RaceCondition[13787:60b] private context: <NSManagedObjectContext: 0x8d5c6b0> set
2014-04-11 20:51:22.924 RaceCondition[13787:60b] setting new timestamp: 2014-04-11 17:52:16 +0000
2014-04-11 20:51:22.924 RaceCondition[13787:3503] thread 0x8d4d290
2014-04-11 20:51:30.123 RaceCondition[13787:60b] main context: <NSManagedObjectContext: 0x8d5c170> already exists
2014-04-11 20:51:22.924 RaceCondition[13787:3503] private context: <NSManagedObjectContext: 0x8d5c6b0> already exists
2014-04-11 20:51:30.123 RaceCondition[13787:3503] merging to private context: <NSManagedObjectContext: 0x8d5c6b0>
2014-04-11 20:51:30.124 RaceCondition[13787:60b] thread 0x8e48cd0
2014-04-11 20:51:30.124 RaceCondition[13787:3503] thread 0x8d4d290
2014-04-11 20:51:30.124 RaceCondition[13787:60b] private context: <NSManagedObjectContext: 0x8d5c6b0> already exists
2014-04-11 20:51:30.125 RaceCondition[13787:3503] private context: <NSManagedObjectContext: 0x8d5c6b0> already exists
2014-04-11 20:51:30.125 RaceCondition[13787:60b] main context: <NSManagedObjectContext: 0x8d5c170> already exists
2014-04-11 20:51:30.126 RaceCondition[13787:3503] thread 0x8d4d290
2014-04-11 20:51:30.126 RaceCondition[13787:3503] private context: <NSManagedObjectContext: 0x8d5c6b0> already exists
** 2014-04-11 20:51:30.126 RaceCondition[13787:3503] merging to private context: <NSManagedObjectContext: 0x8d5c6b0>
2014-04-11 20:51:30.127 RaceCondition[13787:3503] thread 0x8d4d290
2014-04-11 20:51:30.127 RaceCondition[13787:3503] private context: <NSManagedObjectContext: 0x8d5c6b0> already exists
2014-04-11 20:51:36.086 RaceCondition[13787:1303] event timestamp in private context: 2014-04-11 17:50:16 +0000

The starred lines show a possible race condition resulting in the BG operation not to show changes committed by the main context.

Edit:
As can be seen, there are 2 private contexts created:
- used by the BG operation
- created by the parallel save from the main thread

when the main context save only context: is merging changes.

because of the symmetry of the design, this could be used to reproduce a case where the main context is set twice resulting in updates not propagating to the UI.

The risk of a multiple coordinator was given as an additional example for the problems and race conditions involved in the PO design and implementation

As was shown, a possible cause might be a multi "private context" initialisation resulting in the wrong private context being initialised.