pattern to process several NSManagedObjects asynchronously?

I have a set up a timer to report periodically on some event to an outside server with which I communicate asynchronously, the timer fires on the main thread to execute this code:


- (void) sendPendingEvents
{
    NSManagedObjectContext *moc = [self newPrivateContext];
    [moc performBlock:^{
        NSArray *eventsToSend = [TrackingEvent findByAttribute:@"sentToServer" withValue:[NSNumber numberWithBool:NO] andOrderBy:@"timestamp" ascending:YES inContext:moc];
        for (TrackingEvent *event in eventsToSend) {
            [self RESTsendTrackingEvent:event];
        }
    }];
}


Where newPrivateContext creates a new private queue context with the main context as a parent. The idea is that all this should be done asynchronously in the background.

However this fails because RESTsendTrackingEvent: is itself asynchronous (it uses a NSURLSession) and it updates the TrackingEvent (to set its "sentToServer" flag to true) on its completion block.

The problem is that the completion block is called later, when sendPendingEvents has finished running, at which point the managedObjectContext doesn't even exist any more. And indeed I have this Core Data error:

CoreData: error: Mutating a managed object 0x7b7309e0 <x-coredata:///TrackingEvent/t8B8DC387-2B65-4377-A4F4-B82792C89280119> (0x7cbdcce0) after it has been removed from its context.


Which makes complete sense.


I can imagine a couple of ways to fix that (including the cop out to use the main context), but I guess this should be a common occurence.


What would be a nice/elegant pattern to use in cases such as mine?


Thanks


JD

So why aren't you just keeping track of the context you need until the callback completes?


Create a transaction object which contains the following:

  • The managed object to use
  • The list of objects to update
  • Any other identifying information needed for the transaction

and then clean that object up nicely when RESTsendTrackingEvent completes.

I agree with NotMyName.


However, if you are looking for a quick hack that will get around the issue until you can refactor your code, you can do something like this.


I assume your RESTsendTrackingEvent: method is somehow storing the event object as a completion token of sorts, so it can use it later. You can easily create another object as the token like this...


- (void)RESTsendTrackingEvent:(NSManagedObject*)event {
    NSManagedObjectID *objectID = [event objectID];
    typeof(self) __weak weakSelf = self;
    void (^trackingComplete)(void) = ^{
        typeof(self) self = weakSelf;
        NSManagedObjectContext *moc = [self newPrivateContext];
       [moc performBlock:^{
            NSManagedObject *event = [moc existingObjectWithID:objectID error:NULL];
            [event setValue:@YES forKey:@"sentToServer"];
           // save MOC
       }];
    };

    // Do whatever you already do...

    // Assuming you had something like the following that set the event to be the completion token
    [session setCompletionToken:trackingComplete];
}


Then, in your asynchronous completion handler, instead of accessing the "event" object and setting its value, you access the "block" and invoke it since it already knows what to do anyway. This means that the "context" object is a block of code with captured data.


void (^block)(void) = [session completionToken];
if (block) {
   block();
}


If your async API takes a completion block, then all the better... just take that code and put it into the completion block.

Thanks for the suggestions. I ended up doing this, which I think is close to the simplest effective pattern:


- (void) RESTsendTrackingEvent:(TrackingEvent *) event
{
    NSMutableURLRequest *request = <snip>;
    request.HTTPBody = [event jsonRepresentation];;

    // need to get a strong reference to the moc, because caller will exit before we complete, leading to invalidating the TrackingEvent
    // note: NSManagedObjects hold a weak reference to their context (if only to avoid retain cycles)
    NSManagedObjectContext *privateMoc = event.managedObjectContext;
    // but we *also* need to capture that strong reference in the completion block below

    NSURLSession *session = <snip>;
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            NSLog(@"%@ returned Error %@", currentAPI, error);
        } else {
            [privateMoc performBlock:^{
                [event setValue:@YES forKey:@"sentToServer"];  // save the context if needed too
            }];
        }
    }];

    [task resume];
}


The recipe goes like this:


- make a strong reference to the NSManagedObjectContext before the control flow goes asynchronous

- capture that strong reference in the asynchronous completion block, so that the strong reference is released at the end of the completion block rather than at the end of the method itself.


My error went away, and everything now looks fine


Cheers,


Jean-Denis

The reasoning behing holding an object-id over a MOC is that the network operation may take a while to timeout if something is wrong, and you may end up with lots of MOC objects in the interim, if you issue lots of network operations. They are relatively lightweight, so it's probably not a big deal, but I like to hold onto as little as possible while awaiting the completion of a network operation.

pattern to process several NSManagedObjects asynchronously?
 
 
Q