raheel
raheel

Reputation: 1417

Private NSManagedObjectContexts and deleting objects

I have a Core Data stack with a main managed object context with NSMainQueueConcurrencyType.

The user can initiate a task on a managed object that can take a long time, so it is performed on a separate context:

NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:mainMOC];
Person *samePerson = (Person *)[context objectWithID:person.objectID];
[context performBlock:^{
    // BLOCK 1
    // do lots of work

    // then update the managed object
    samePerson.value = someCalculatedValue;

    // save the private context
    NSError *error;
    if (![context save:&error]) {
        NSLog(@"Error: %@", error);
    }

    [mainMOC performBlock:^{
        NSError *error;
        if (![mainMOC save:&error]) {
            NSLog(@"Error saving: %@", error);
        }
    }];
}];

This works fine, and the main MOC gets updated properly, NSFetchedResultsController hooked up to it perform properly, etc.

The problem is with deleting. I have this setup to delete objects:

NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:mainMOC];
[context performBlock:^{
    // BLOCK 2
    NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:@"Person"];
    NSError *error;
    NSArray *all = [context executeFetchRequest:request error:&error];
    if (!all) {
        NSLog(@"Error fetching: %@", error);
    } else {
        for (NSManagedObject *person in all) {
            [context deleteObject:person];
        }
        NSError *error;
        if (![context save:&error]) {
            NSLog(@"Error saving: %@", error);
        }

        [mainMOC performBlock:^{
            NSError *error;
            if (![mainMOC save:&error]) {
                NSLog(@"Error saving: %@", error);
            }
        }];
    }
}];

Now if I do this delete operation (Block 2) during the time it takes to perform the long-duration task (Block 1), then the delete operation finishes quickly, and saves to main context. After a while Block 1 finishes, and when it saves the mainMOC at its end, we get a seemingly obvious crash:

CoreData could not fulfill a fault for ...

My question is: how do I perform a task such as Block 1 with the possibility of its object being deleted?

Upvotes: 1

Views: 226

Answers (1)

Rob Glassey
Rob Glassey

Reputation: 2257

If these are both background tasks that cannot run at the same time, try using semaphores to protect access to them.

eg. for an instance variable:

dispatch_semaphore_t _backgroundProcessingSemaphore;

Lazily initialised using something like:

- (dispatch_semaphore_t)backgroundProcessingSemaphore
{
    if (!_backgroundProcessingSemaphore) {
        _backgroundProcessingSemaphore = dispatch_semaphore_create(1);
    }
    return _backgroundProcessingSemaphore;
}

Surround the critical code with:

dispatch_semaphore_wait(self.backgroundProcessingSemaphore, DISPATCH_TIME_FOREVER);
// Critical code
dispatch_semaphore_signal(self.backgroundProcessingSemaphore);

Only one critical section of code can then run at any point in time. The block that calls dispatch_semaphore_wait will block if the semaphore is already taken, until it is freed up.

You also probably want to think about splitting your long-duration task up so that it will run in discrete batches if you're not already doing so - this is useful if the long running background task timer is about to expire while you still have work to do - you can stop and restart from the appropriate point on next launch.

Other options would involve forcing a save on block 1 before block 2 saves itself, but this starts to get messy. Much easier to ensure the two competing blocks cannot overlap.

Upvotes: 1

Related Questions