Bug: A managed object relationship that contains multiple copies of the same object.

Thanks very much for reading.

Our app uses multiple NSManagedObjectContexts on multiple threads. We do the following...


  1. We want to run a background task that downloads data.
  2. We spin up a background thread.
  3. That thread creates a child context from the main one.
  4. Download some JSON
  5. Use that new data to update managed objects, creating them when necessary, object identity is tracked via a "uuid" string property.
  6. Save that background thread's context.
  7. Background thread goes away, taking the child context with it.


On the foreground thread we are seeing a rare probably-a-timing-bug where some of our objects clone themselves in the UI. We see two copies of things in very strange ways. Different sets of objects double at different times, and they have a tendency to go away again if you wait a bit. Below is an LLDB session that demonstrates that I have an NSManagedObject that has a relationship with duplicates in it. Those duplicates have the same objectID.


I've found this to be reasonably reproducible in our app by picking an object and having it sync with the network version every few seconds.


My question is: what can I do about this? How can I prevent it? I'd love to file a radar but I can be a bit more helpful if I can understand this problem a little better and exactly what I'm doing that is causing it.


Thanks again.


// Here is an object, it is a NSManagedObject subclass
(lldb) po [order class]
Order
(lldb) po [order superclass]
OfficeV3Syncable
(lldb) po [[order superclass] superclass]
NSManagedObject

// here is one of it's relationships to another managed object: "OrderLine"
(lldb) expression (NSArray*)[[order orderLines] allObjects]
(NSArray *) $15 = 0x15b703e0
(lldb) po [$15 count]
8

// here is one of those objects, let's look at it
(lldb) expression $15[1]
(OrderLine_OrderLine_ *) $17 = 0x158f12e0
(lldb) po [$17 objectID]
0x158ed960 <x-coredata://E17717A3-D026-4EC1-AE65-F33741008328/OrderLine/p37>
(lldb) p (BOOL)[[$17 objectID] isTemporaryID]
(BOOL) $21 = NO

// here's another one, a few interesting things:
// 1. The two objects are at different memory addresses
// 2. The managedObjectIDs match
// 3. Even the memory address of the managedObjectIDs matches
(lldb) expression $15[7]
(OrderLine_OrderLine_ *) $19 = 0x15c22c90
(lldb) po [$19 objectID]
0x158ed960 <x-coredata://E17717A3-D026-4EC1-AE65-F33741008328/OrderLine/p37>
(lldb) p (BOOL)[[$19 objectID] isTemporaryID]
(BOOL) $22 = NO

Oh and some context:


Language: ObjC

XCode: Version 6.4 (6E35b)

OSX: 10.10.4 (14E46)

Deployment target: iOS 8.0

I may have figured this out.


  1. Say I have two NSManagedObject subclasses. Parent and Child.
  2. Parent hasMany Child
  3. Here's a simplified psudo-code version of my trouble code:


NSObject *parentObject = // got this from earlier JSON deserialization, may or may not be a new object

NSDictionary *childData = // deserialized data from a server representing a child object
Child *childObject = [Child grabChildFromTheDatabaseForID:childDate[@"uuid"]]; // core data fetch
if(!childObject) {
    // we've never seen this object before, make a new one
    childObject = [Child makeANewEntityInstanceInContext:parent.managedObjectContext];
}

// just a bunch of putting values into core data properties
[childObject setAllYourPropertiesFromServerData:childData];

// this is an automatically generated core data method
[parentObject addChildrenObject:childObject];


The problem is on that last line. Often times childObject is already a member of parentObject.children. The trouble comes when the copy we already have has a permanent ID and the incoming one (childObject above) has a temporary ID. Stated another way.


Child *sameChild = [parentObject childObjectWithUUID:childObject.uuid];
BOOL isThereGoingToBeTrouble = (sameChild.objectID.isTemporaryID != childObject.objectID.isTemporaryID);


In this situation I have two copies of the same object, but thieir managedObjectIDs are not equal (they aren't isEqual:) and they don't have the same hash. I end up with sort-of-two of them.


Working on a workaround now.

A little more info.


It is not necessary for the copies of the object to disagree on their temporaryness for this bug to show up. I saw it happen with both objects being non-temporary.


Manually looping over the collections and removing duplicates any time this particular background thread changes any relationship seems to have done the trick. Yuck.

Is the inverse relationship properly set up?

Apologies I didn't get the email notification on this reply.


Yes it is. And my workaround fix turns out not to work. I think I'm going to need to open an Apple dev support ticket on this one.

Which version of iOS 8 (specific version number that you're testing on, like 8.4.1 on an iPad 3) are you seeing this on?


And have you tried the silly sounding work around of setting the parent property on the child? Or, are you setting the parent property on the child? If you have the to-many and to-one relationships set up as inverses, setting the parent on the child should take care of updating the children relationship automatically.

I have similar scenario and the same problem on iOS 9, too. Did you find any fix meanwhile? Or at least an explanation?


P.S. In my case the relationship is one-to-one:


parentEntity <-> childEntity

I end up with 2 duplicated children having the same parent (one has temporaryID, the other permanent)

Bug: A managed object relationship that contains multiple copies of the same object.
 
 
Q