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:
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.