Ethan Fang
Ethan Fang

Reputation: 1269

NSOperationQueue, CoreData and MagicalRecord sync issue

I create a NSOperationQueue for all the operations regarding inserting/updating/deleting/searching of one single table. I use MagicalRecord for Core data operation. But I have some problem with the syncing. A simplified example is as following.

Eg. A table called person and a column inside person called like. When user clicks a button, the like will increase by one. I do it like

[SameBackgroundQueue addOperationWithBlock:^{
    User *user = [User MR_findFirstWithPredicate:some_predicate];
    user.like += 1;
    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextForCurrentThread];
    [localContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
        [SameBackgroundQueue addOperationWithBlock:^{
            [self syncWithServer];//This can take time
    }];
}];

However, if the user clicks fast, the like will not be correct, because MR_findFirstWithPredicate can get dirty record. The problem stems from that NSOperationQueue can employee different threads, and MR_findFirstWithPredicate uses the context from the current thread. So potentially, it will try to get user from different NSManagedObjectContext and hence it will get dirty data.

Of course if we use mainQueue, we will not have any problem. However, how can I use background thread and also make sure I don't have dirty record problem. Seems the whole problem can be solved if I run on one particular thread rather than user NSOperationQueue. Should I use GCD instead?


In my previous project, I am using

[self createManagedObjectContextWithParent:self.mainQueueManagedObjectContext andConcurrencyType:NSPrivateQueueConcurrencyType];

and

[managedObjectContext save:&error];
if (managedObjectContext.parentContext) {
    [managedObjectContext.parentContext performBlock:^{
        NSError *parentError = nil;
        [managedObjectContext.parentContext save:&parentError];
    }];
}

I know there are WWDC videos talking about NSManagedObjectContext and concurrent context, etc. But if I use that, I can not use MagicRecord.

Any suggestion will be highly appreciated.


Actually I find a more efficient way to do it. Please correct me if I am doing it wrong.

I actually create a common singleton context [NSManagedObjectContext MR_context] to be shared for User OperationQueue. So even all threads access the same context, they will not get dirty data.

There will still be glitches, eg, what if two threads are changing the same object of the same context. That is normally a very rare case, but I will see how it goes. I may set max concurrent thread to be one, just to avoid the situation. I am not sure whether it will decrease the performance. Will update the progress tomorrow.

Upvotes: 2

Views: 1009

Answers (2)

casademora
casademora

Reputation: 69687

I would recommend changing your operation block to something more like this:

[TheSameQueue addOperationWithBlock:^{
    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_context];

    User *user = [User MR_findFirstWithPredicate:some_predicate inContext:localContext];
    user.like += 1;

    [localContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
        [self syncWithServer];//This can take time
    }];
}];

You were doing all background operations on the default context, which you can think of as the main thread context. This will work 99% of the time, in general, but leads to random deadlocks and crashes. Also, MR_contextForCurrentThread is deprecated because it also causes issues with queues, namely using the wrong context for a thread.

Upvotes: 3

Dan Shelly
Dan Shelly

Reputation: 6011

To avoid this race condition your could limit your NSOperationQueue to a single operation at a time by setting:

[TheSameQueue setMaxConcurrentOperationCount:1];

Rendering it a serial execution queue.

You could also create a serial queue using GCD (but then you will have a harder time cancelling and setting dependencies for operations).

(both use GCD in the background)

I would execute the server sync in a different queue so it will not hinder user experience locally (you could also make the local changes on the main thread, and only sync in the background)

Your sync to the server might be problematic as the local changes are not tracked and the need to sync an object to the server is not persisted between application launches (save to store succeeded but sync to server interrupted/failed, etc ...).

Upvotes: 2

Related Questions