How to properly propagate a deletion from an NSManagedObjectContext main thread to a child context in a background thread?

I am trying to figure out how to solve the following situation.

  • There is a main thread NSManagedObjectContext with NSMainQueueConcurrencyType . It generates several background threads, giving them the NSManagedObjectID some object that they will work on.
  • Background threads do some work (for example, send object data to the server, receive a response, and update the object accordingly). Themes use child contexts with NSConfinenmentConcurrencyType
  • In the meantime, the user removes the object from the context of the main thread (via the user interface).
  • Background contexts should be notified of this and handle the situation in order to exclude the exception "cannot make an error" when saving the background context.

I thought that the main context (some kind of user object that controls it) could keep a record of the identifiers of the objects that were deleted during the life cycle of the background thread (or, more precisely, between creating the background context and finally saving the background context). Then the background context would have to execute deleteObject: on these objects immediately before saving it. And everything will go smoothly.

To ensure that the main context does not delete the object when the background thread completes the deletion of the objects and is about to call save: in its context, and to ensure that the main context delete does not happen after the child object is created, but before the child thread logged in to be "notified" of deleted objects, I used several mutex locks and came up with the following concept code:

 @property (nonatomic, strong) id deleteLock; @property (nonatomic, strong) NSMutableDictionary *deletedObjectIdsPerThreadLifetime; - (void)coreDataDeleteSyncExample { static int lastThreadNo = 0; self.deleteLock = [[NSObject alloc] init]; self.deletedObjectIdsPerThreadLifetime = [[NSMutableDictionary alloc] init]; // main context is created using NSMainQueueConcurrencyType NSManagedObjectContext *mainContext = [self mainContext]; NSManagedObjectID *myObjectId = nil; // creating the Object Order *order = (Order*)[NSEntityDescription insertNewObjectForEntityForName:@"Order" inManagedObjectContext:mainContext]; Payment *payment = (Payment*)[NSEntityDescription insertNewObjectForEntityForName:@"Payment" inManagedObjectContext:mainContext]; if (order) { [payment setOrder:order]; [payment setAmount:[NSDecimalNumber decimalNumberWithString:@"103"]]; NSError *error = nil; if (![mainContext save:&error]) { NSLog(@"main context save failed"); } myObjectId = [order objectID]; // so I have non-temporary objectId here that I can pass around } int threadNo; for (threadNo = lastThreadNo ; threadNo < 50+lastThreadNo; threadNo++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSNumber *threadNumber = [NSNumber numberWithInt:threadNo]; NSManagedObjectContext *bckContext = nil; NSError *error = nil; @synchronized(self.deleteLock) { bckContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType]; bckContext.parentContext = mainContext; [self.deletedObjectIdsPerThreadLifetime setObject:[NSMutableSet set] forKey:threadNumber]; NSLog(@"Bck #%d created delete list/dict", threadNo); } Order *order = (Order*)[bckContext existingObjectWithID:myObjectId error:&error]; for (int i = 0; i < 30; i++) { order.status = [NSString stringWithFormat:@"some status set by background thread, %d/%d", threadNo, i]; NSLog(@"(dont clutter log):%d/%@", threadNo, order.status); } // background context now is going to save the order, but before that it deletes // from it all the objects that have been deleted from the main context in the meantime // we make it @synchronized call to make sure mainContext has no chances to delete // additional objects after we delete the ones from the set // and before we save background NSLog(@"Bck #%d saving context...", threadNo); @synchronized(self.deleteLock) { NSSet *objsToDelete = [self.deletedObjectIdsPerThreadLifetime objectForKey:threadNumber]; for (NSManagedObjectID *objectId in objsToDelete) { NSManagedObject *obj = [bckContext objectWithID:objectId]; NSLog(@"Bck #%d deleted obj %@ because it was on the list", threadNo,objectId); [bckContext deleteObject:obj]; } if (objsToDelete == nil) { NSLog(@"Bck #%d is NOT included in delete dictionary list.", threadNo); } else { NSLog(@"Bck #%d has empty list of objs to delete.", threadNo); } NSLog(@"Bck #%d JUST before save...", threadNo); // saving bck outside the lock is wrong error = nil; if (![bckContext save:&error]) { NSLog(@"Bck context #%d failed to save: %@", threadNo, error); } else { NSLog(@"Bck #%d saved its context!", threadNo); } } // saving main context outside the lock [mainContext performBlockAndWait:^{ NSError *error = nil; NSLog(@"Main thread will save context (requested by Bck #%d)", threadNo); if (![mainContext save:&error]) { NSLog(@"main context save failed"); } else { NSLog(@"main context saved (requested by bck #%d)", threadNo); } }]; }); } lastThreadNo = threadNo; // now let delete that object in the meantime on the main thread, and save the main context after a while dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(150 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ Order *o = (Order*)[mainContext objectWithID:myObjectId]; NSLog(@"Main - will delete..."); //@synchronized(self.deleteLock) { objc_sync_enter(self.deleteLock); for (NSNumber *threadNumber in self.deletedObjectIdsPerThreadLifetime) { NSMutableSet *deletedIds = [self.deletedObjectIdsPerThreadLifetime objectForKey:threadNumber]; [deletedIds addObject:myObjectId]; } NSLog(@"Main -deleting- %@", myObjectId); [mainContext deleteObject:o]; NSLog(@"Main -deleted- %@", myObjectId); //} dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(150 * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ DLog(@"AND NOW WE SAVE MAIN!"); NSError *error = nil; if (![mainContext save:&error]) { NSLog(@"main context save failed"); } else { NSLog(@"main context saved (requested by main context)"); } objc_sync_exit(self.deleteLock); }); }); } 

It turned out that the code has several problems: 1. This is a deadlock. When background threads start saving "transactions", it gets a lock, and then if mainThread manages to meet a locked synchronized block @. The background then proceeds to the save: call. There it seems that CoreData wants to keep the child in the main context, so he is trying to use this context. Since it can only be used in the main thread, and the main thread is blocked by the lock received by the background thread, we have a dead end. 2. It still crashes with "cannot make a mistake." This only happens sometimes when the main context is deleted and retained only before creating the background context and receiving the object. Usually in this situation this object is equal to zero. But sometimes it’s not (why ???), and we got a failure while maintaining the background context, as in this situation:

 2014-11-27 14:00:13.179 ConcurrentCoreData[70490:1403] Bck #0 created delete list/dict 2014-11-27 14:00:13.186 ConcurrentCoreData[70490:1403] (dont clutter log):0/some status set by background thread, 0/0 2014-11-27 14:00:13.187 ConcurrentCoreData[70490:1403] (dont clutter log):0/some status set by background thread, 0/1 2014-11-27 14:00:13.189 ConcurrentCoreData[70490:1403] (dont clutter log):0/some status set by background thread, 0/2 2014-11-27 14:00:13.189 ConcurrentCoreData[70490:1403] (dont clutter log):0/some status set by background thread, 0/3 2014-11-27 14:00:13.190 ConcurrentCoreData[70490:2c07] Bck #1 created delete list/dict 2014-11-27 14:00:13.190 ConcurrentCoreData[70490:1403] (dont clutter log):0/some status set by background thread, 0/4 2014-11-27 14:00:13.192 ConcurrentCoreData[70490:3907] Bck #2 created delete list/dict 2014-11-27 14:00:13.191 ConcurrentCoreData[70490:1403] (dont clutter log):0/some status set by background thread, 0/5 (...) 2014-11-27 14:00:13.309 ConcurrentCoreData[70490:4b03] (dont clutter log):7/some status set by background thread, 7/10 2014-11-27 14:00:13.309 ConcurrentCoreData[70490:2c07] (dont clutter log):1/some status set by background thread, 1/23 2014-11-27 14:00:13.311 ConcurrentCoreData[70490:1403] (dont clutter log):0/some status set by background thread, 0/29 2014-11-27 14:00:13.329 ConcurrentCoreData[70490:90b] Main - will delete... 2014-11-27 14:00:13.333 ConcurrentCoreData[70490:4e03] Bck #8 created delete list/dict 2014-11-27 14:00:13.316 ConcurrentCoreData[70490:4b03] (dont clutter log):7/some status set by background thread, 7/11 2014-11-27 14:00:13.365 ConcurrentCoreData[70490:5003] Bck #9 created delete list/dict 2014-11-27 14:00:13.367 ConcurrentCoreData[70490:5103] Bck #10 created delete list/dict 2014-11-27 14:00:13.367 ConcurrentCoreData[70490:5203] Bck #11 created delete list/dict 2014-11-27 14:00:13.366 ConcurrentCoreData[70490:4b03] (dont clutter log):7/some status set by background thread, 7/12 2014-11-27 14:00:13.316 ConcurrentCoreData[70490:2c07] (dont clutter log):1/some status set by background thread, 1/24 2014-11-27 14:00:13.311 ConcurrentCoreData[70490:3807] (dont clutter log):3/some status set by background thread, 3/20 2014-11-27 14:00:13.312 ConcurrentCoreData[70490:3b03] (dont clutter log):4/some status set by background thread, 4/19 2014-11-27 14:00:13.316 ConcurrentCoreData[70490:3c03] (dont clutter log):5/some status set by background thread, 5/18 2014-11-27 14:00:13.314 ConcurrentCoreData[70490:3907] (dont clutter log):2/some status set by background thread, 2/22 2014-11-27 14:00:13.312 ConcurrentCoreData[70490:4603] (dont clutter log):6/some status set by background thread, 6/17 2014-11-27 14:00:13.365 ConcurrentCoreData[70490:1403] Bck #0 saving context... 2014-11-27 14:00:13.369 ConcurrentCoreData[70490:90b] Main -deleting- 0x8b24cd0 <x-coredata://06DFA035-E3DF-497C-89B4-20E845A09712/Order/p549> (...) 2014-11-27 14:00:13.372 ConcurrentCoreData[70490:90b] Main -deleted- 0x8b24cd0 <x-coredata://06DFA035-E3DF-497C-89B4-20E845A09712/Order/p549> (...) 2014-11-27 14:00:13.420 ConcurrentCoreData[70490:2c07] Bck #1 saving context... (...) 2014-11-27 14:00:13.453 ConcurrentCoreData[70490:3907] Bck #2 saving context... (...) 2014-11-27 14:00:13.475 ConcurrentCoreData[70490:3807] Bck #3 saving context... (...) 2014-11-27 14:00:13.488 ConcurrentCoreData[70490:3b03] Bck #4 saving context... (...) 2014-11-27 14:00:13.496 ConcurrentCoreData[70490:3c03] Bck #5 saving context... (...) 2014-11-27 14:00:13.558 ConcurrentCoreData[70490:90b] __43-[ViewController coreDataDeleteSyncExample]_block_invoke_2178 [Line 260] AND NOW WE SAVE MAIN! 2014-11-27 14:00:13.559 ConcurrentCoreData[70490:4603] (dont clutter log):6/some status set by background thread, 6/28 (...) 2014-11-27 14:00:13.564 ConcurrentCoreData[70490:4e03] (dont clutter log):8/some status set by background thread, 8/13 2014-11-27 14:00:13.565 ConcurrentCoreData[70490:90b] main context saved (requested by main context) 2014-11-27 14:00:13.565 ConcurrentCoreData[70490:4603] (dont clutter log):6/some status set by background thread, 6/29 2014-11-27 14:00:13.566 ConcurrentCoreData[70490:5003] (dont clutter log):9/some status set by background thread, 9/13 (...) 2014-11-27 14:00:13.663 ConcurrentCoreData[70490:2c07] Bck #1 saved its context! 2014-11-27 14:00:13.664 ConcurrentCoreData[70490:5003] (dont clutter log):9/some status set by background thread, 9/24 2014-11-27 14:00:13.667 ConcurrentCoreData[70490:90b] Main thread will save context (requested by Bck #1) 2014-11-27 14:00:13.667 ConcurrentCoreData[70490:90b] main context saved (requested by bck #1) 2014-11-27 14:00:13.667 ConcurrentCoreData[70490:5003] (dont clutter log):9/some status set by background thread, 9/25 2014-11-27 14:00:13.668 ConcurrentCoreData[70490:3907] Bck #2 deleted obj 0x8b24cd0 <x-coredata://06DFA035-E3DF-497C-89B4-20E845A09712/Order/p549> because it was on the list 2014-11-27 14:00:13.668 ConcurrentCoreData[70490:5003] (dont clutter log):9/some status set by background thread, 9/26 2014-11-27 14:00:13.668 ConcurrentCoreData[70490:3907] Bck #2 has empty list of objs to delete. 2014-11-27 14:00:13.668 ConcurrentCoreData[70490:5003] (dont clutter log):9/some status set by background thread, 9/27 2014-11-27 14:00:13.669 ConcurrentCoreData[70490:5003] (dont clutter log):9/some status set by background thread, 9/28 2014-11-27 14:00:13.669 ConcurrentCoreData[70490:5003] (dont clutter log):9/some status set by background thread, 9/29 2014-11-27 14:00:13.670 ConcurrentCoreData[70490:5003] Bck #9 saving context... 2014-11-27 14:00:13.668 ConcurrentCoreData[70490:3907] Bck #2 JUST before save... 2014-11-27 14:00:13.666 ConcurrentCoreData[70490:4e03] (dont clutter log):8/some status set by background thread, 8/18 2014-11-27 14:00:13.671 ConcurrentCoreData[70490:3907] Bck #2 saved its context! 2014-11-27 14:00:13.671 ConcurrentCoreData[70490:5a03] Bck #12 created delete list/dict 2014-11-27 14:00:13.672 ConcurrentCoreData[70490:5b03] Bck #13 created delete list/dict 2014-11-27 14:00:13.672 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/0 2014-11-27 14:00:13.673 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/1 (!! and here queue #13 HAS the object! But it was deleted from main context and the main context was saved before we spawned that child context!) 2014-11-27 14:00:13.673 ConcurrentCoreData[70490:5b03] (dont clutter log):13/some status set by background thread, 13/0 2014-11-27 14:00:13.674 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/2 2014-11-27 14:00:13.674 ConcurrentCoreData[70490:5b03] (dont clutter log):13/some status set by background thread, 13/1 2014-11-27 14:00:13.674 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/3 2014-11-27 14:00:13.674 ConcurrentCoreData[70490:5b03] (dont clutter log):13/some status set by background thread, 13/2 2014-11-27 14:00:13.673 ConcurrentCoreData[70490:5c03] Bck #14 created delete list/dict 2014-11-27 14:00:13.675 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/4 2014-11-27 14:00:13.675 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/5 2014-11-27 14:00:13.676 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/6 2014-11-27 14:00:13.676 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/7 2014-11-27 14:00:13.676 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/8 2014-11-27 14:00:13.675 ConcurrentCoreData[70490:5b03] (dont clutter log):13/some status set by background thread, 13/3 2014-11-27 14:00:13.676 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/9 2014-11-27 14:00:13.677 ConcurrentCoreData[70490:5a03] (dont clutter log):12/some status set by background thread, 12/10 (!!! QUEUE #14 AS WELL???) 2014-11-27 14:00:13.675 ConcurrentCoreData[70490:5c03] (dont clutter log):14/some status set by background thread, 14/0 (…) 2014-11-27 14:00:14.503 ConcurrentCoreData[70490:7a03] (dont clutter log):44/(null) 2014-11-27 14:00:14.504 ConcurrentCoreData[70490:7f03] (dont clutter log):49/(null) 2014-11-27 14:00:14.505 ConcurrentCoreData[70490:90b] Main thread will save context (requested by Bck #9) 2014-11-27 14:00:14.505 ConcurrentCoreData[70490:7e03] (dont clutter log):48/(null) 2014-11-27 14:00:14.505 ConcurrentCoreData[70490:7b03] (dont clutter log):45/(null) 2014-11-27 14:00:14.505 ConcurrentCoreData[70490:7d03] (dont clutter log):47/(null) 2014-11-27 14:00:14.505 ConcurrentCoreData[70490:5b03] Bck #13 has empty list of objs to delete. 2014-11-27 14:00:14.506 ConcurrentCoreData[70490:7903] (dont clutter log):43/(null) 2014-11-27 14:00:14.506 ConcurrentCoreData[70490:7c03] (dont clutter log):46/(null) 2014-11-27 14:00:14.509 ConcurrentCoreData[70490:90b] main context saved (requested by bck #9) 2014-11-27 14:00:14.508 ConcurrentCoreData[70490:7a03] Bck #44 saving context... 2014-11-27 14:00:14.510 ConcurrentCoreData[70490:7e03] (dont clutter log):48/(null) 2014-11-27 14:00:14.510 ConcurrentCoreData[70490:7b03] (dont clutter log):45/(null) (QUEUE #13 tries to save and it crashes!) 2014-11-27 14:00:14.510 ConcurrentCoreData[70490:5b03] Bck #13 JUST before save... 2014-11-27 14:00:14.510 ConcurrentCoreData[70490:7d03] (dont clutter log):47/(null) 2014-11-27 14:00:14.508 ConcurrentCoreData[70490:7f03] (dont clutter log):49/(null) 2014-11-27 14:00:14.511 ConcurrentCoreData[70490:7903] Bck #43 saving context... 2014-11-27 14:00:14.511 ConcurrentCoreData[70490:7c03] (dont clutter log):46/(null) 2014-11-27 14:00:14.514 ConcurrentCoreData[70490:7e03] (dont clutter log):48/(null) 2014-11-27 14:00:14.516 ConcurrentCoreData[70490:90b] *** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x8b24cd0 <x-coredata://06DFA035-E3DF-497C-89B4-20E845A09712/Order/p549>'' *** First throw call stack: ( 0 CoreFoundation 0x018001e4 __exceptionPreprocess + 180 1 libobjc.A.dylib 0x0157f8e5 objc_exception_throw + 44 2 CoreData 0x01a8cbeb _PFFaultHandlerLookupRow + 2715 3 CoreData 0x01abee88 -[NSFaultHandler fulfillFault:withContext:] + 40 4 CoreData 0x01b33169 -[NSManagedObject(_NSInternalMethods) _updateFromRefreshSnapshot:includingTransients:] + 265 5 CoreData 0x01ac7902 -[NSManagedObjectContext(_NestedContextSupport) _copyChildObject:toParentObject:fromChildContext:] + 994 6 CoreData 0x01ac71e8 -[NSManagedObjectContext(_NestedContextSupport) _parentProcessSaveRequest:inContext:error:] + 1480 7 CoreData 0x01b3fa14 __82-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]_block_invoke + 676 8 CoreData 0x01ac1b81 internalBlockToNSManagedObjectContextPerform + 17 9 libdispatch.dylib 0x01f784d0 _dispatch_client_callout + 14 10 libdispatch.dylib 0x01f67439 _dispatch_barrier_sync_f_slow_invoke + 80 11 libdispatch.dylib 0x01f784d0 _dispatch_client_callout + 14 12 libdispatch.dylib 0x01f66726 _dispatch_main_queue_callback_4CF + 340 13 CoreFoundation 0x0186543e __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 14 14 CoreFoundation 0x017a65cb __CFRunLoopRun + 1963 15 CoreFoundation 0x017a59d3 CFRunLoopRunSpecific + 467 16 CoreFoundation 0x017a57eb CFRunLoopRunInMode + 123 17 GraphicsServices 0x03b0a5ee GSEventRunModal + 192 18 GraphicsServices 0x03b0a42b GSEventRun + 104 19 UIKit 0x0023ff9b UIApplicationMain + 1225 20 ConcurrentCoreData 0x00009d7d main + 141 21 libdyld.dylib 0x021ab725 start + 0 ) libc++abi.dylib: terminating with uncaught exception of type _NSCoreDataException (lldb) 

I understand the cause of the first problem (dead end). I have no idea how to solve it, I think that a user lock like this is not possible when using context child context contexts.

But the second is really strange. Why is the object not null? After all the master data has been deleted and saved before creating the child context. Why usually I get nil there, but sometimes I get an object? Is this a cache problem? Can I not trust Core Data when returning nil in a child context for an object that was deleted in the main context (and saved!) Until when the detailed context was created ?! Is my decision fundamentally wrong?

What is the right way to handle this situation when background contexts have to deal with deleting the main context. I have the feeling that this whole function of the main / child context is really nice and easy to use unless you start deleting objects in the main contexts. Then all this becomes useless, and we still have to resort to storing and merging contexts.

+5
source share

Source: https://habr.com/ru/post/915658/


All Articles