I'm having a race condition problem in trying to create or update managed objects in child contexts of a common parent.
Assume the following setup:
NSPrivateQueueConcurrencyType
. This is also the only context with a persistent store coordinator pointing to the backing SQL.NSMainQueueConcurrencyType
User
. Assume User
has 2 attributes: id
and name
. Also, User
has a to-many relationship friends
.The flow for retrieving a user along with their attributes and relationships would go something like this (I'm simplifying and using a bit of pseudo code cause the actual implementation is lengthy and complex, but this should get the point across):
The context in charge of dealing with attributes does this:
[childContext1 performBlock:^{
// id jsonData = GET http://myserver.com/users/1
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:user.entity.name];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"id = %i", [jsonData[@"id"] intValue]];
NSArray *results = [childContext1 executeFetchRequest:fetchRequest error:&error];
NSManagedObject *newOrExistingObject = nil;
BOOL existsInContext = (BOOL)[results count];
if (!existsInContext) {
newOrExistingObject = [NSEntityDescription insertNewObjectForEntityForName:e.name inManagedObjectContext:childContext1];
}
else {
newOrExistingObject = results[0];
}
// at this point, the id and name of the user would be set.
[newOrExistingObject setValuesForKeysWithDictionary:jsonData];
[childContext1 save:nil];
[mainContext performBlock:^{
[mainContext obtainPermanentIDsForObjects:mainContext.insertedObjects.allObjects error:nil];
[mainContext save:nil];
[saveContext performBlock:^{
[self.saveContext save:nil];
}];
}];
}};
The context in charge of dealing with the relationship would do this:
[childContext2 performBlock:^{
// id jsonData = GET http://myserver.com/users/friends/
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:user.entity.name];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"id = %i", [jsonData[@"id"] intValue]];
NSArray *results = [childContext2 executeFetchRequest:fetchRequest error:&error];
NSManagedObject *newOrExistingObject = nil;
BOOL existsInContext = (BOOL)[results count];
if (!existsInContext) {
newOrExistingObject = [NSEntityDescription insertNewObjectForEntityForName:e.name inManagedObjectContext:childContext2];
}
else {
newOrExistingObject = results[0];
}
// at this point, the 'friends' relationship would be set
[newOrExistingObject setValuesForKeysWithDictionary:jsonData];
[childContext2 save:nil];
[mainContext performBlock:^{
[mainContext obtainPermanentIDsForObjects:mainContext.insertedObjects.allObjects error:nil];
[mainContext save:nil];
[saveContext performBlock:^{
[self.saveContext save:nil];
}];
}];
}};
The problem with the above approach is that in order to "create or update", I need to figure out if the object in question already exists (which I attempt to do so by doing a fetch by id
since this is the attribute I base equivalence on).
As it stands, sometimes one of the child contexts will be done doing its thing and by the time the other context attempts fetching an object with the id in question, it'll get the object previously created by the other context, update it and everything will go fine. But sometimes, whichever child context executes second does not find the object previously created by the child who executed first. So it creates another new object with the same id
. By the time the whole thing is done and both children propagated their objects to the main context and the main context in turn to the save context, I end up with two objects with different objectID
but same id
: one will have the attributes and the other the relationship. My guess is this is because by the time the second context tries to fetch the object, either:
Basically what I need is a way to either synchronize each child context's fetch, so that this cannot happen until whichever child executes first and creates the object has saved and in turn the object has been propagated to the main context. Or, perhaps better yet, a way for the main context to know that any number of objects of the same entity class and with the same id
attribute value are equivalent (despite their objectID
s being different since they were created in different contexts in parallel) and so should be merged into a single one.
Any thoughts?
dispatch_semaphore_t
:p
I wasn't aware that I could use GCD semaphores inside an NSManagedObjectContext
's -performBlock
.
I ended up creating a mutex (a binary semaphore), so that the child blocks do:
[childContext performBlock:^{
// id jsonData = GET http://myserver.com/<whateverData>
dispatch_semaphore_wait(_s, DISPATCH_TIME_FOREVER);
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:user.entity.name];
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"id = %i", [jsonData[@"id"] intValue]];
NSArray *results = [childContext2 executeFetchRequest:fetchRequest error:&error];
NSManagedObject *newOrExistingObject = nil;
BOOL existsInContext = (BOOL)[results count];
if (!existsInContext) {
newOrExistingObject = [NSEntityDescription insertNewObjectForEntityForName:e.name inManagedObjectContext:childContext];
}
else {
newOrExistingObject = results[0];
}
[newOrExistingObject setValuesForKeysWithDictionary:jsonData];
[childContext obtainPermanentIDsForObjects:childContext.insertedObjects.allObjects error:nil];
[childContext save:nil];
dispatch_semaphore_signal(_s);
[mainContext performBlock:^{
[mainContext save:nil];
[saveContext performBlock:^{
[self.saveContext save:nil];
}];
}];
}};
And this did the trick