Amro Younes
Amro Younes

Reputation: 1301

Core Data Managed Context not saving in multi-threaded environment

While trying to learn more about Core Data and multithreading, I wrote an app that fails to save to core data. First some background. Before my view appears, I create managed context. This happens in the main thread:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    if (!self.managedObjectContext) [self useDocument];

}

- (void)useDocument
{
    NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    url = [url URLByAppendingPathComponent:MY_DOCUMENT];
    UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:url];

    if (![[NSFileManager defaultManager] fileExistsAtPath:[url path]]) {
        [document saveToURL:url
           forSaveOperation:UIDocumentSaveForCreating
          completionHandler:^(BOOL success) {
              if (success) {
                  NSLog(@"@@@AMRO--->managedObjectContext has been setup");
                  self.managedObjectContext = document.managedObjectContext;
                  [self refresh];
              }
          }];
    } else if (document.documentState == UIDocumentStateClosed) {
        [document openWithCompletionHandler:^(BOOL success) {
            if (success) {
                NSLog(@"@@@AMRO--->managedObjectContext has been opened");
                self.managedObjectContext = document.managedObjectContext;
                [self refresh];
            }
        }];
    } else {
        NSLog(@"@@@AMRO--->managedObjectContext has been set");
        self.managedObjectContext = document.managedObjectContext;
    }
}

When my view appears, I fetch data from a client using this approach. The reason I do this is because I do not want my view to be blocked as my client refresh is happening:

        dispatch_queue_t fetchQ = dispatch_queue_create("Client Fetch", NULL);
        dispatch_async(fetchQ, ^{
            [self.managedObjectContext performBlock:^{
               dispatch_async(dispatch_get_main_queue(), ^{
                    [self reload];
                    dispatch_async(fetchQ, ^{
                        [self refreshClientInformation];
                    });
                });
            }];
        });

The call to refreshClientInformation, takes about 2 minutes to retrieve information, however, I want the view to load in the meantime with whatever I have. The above approach prevents me from being able to save to context despite the explicit call to [self.managedContext save:&error] in refreshClientInformation after each client data update. refreshPersonalClientInformations executes in the "Client Fetch" thread queue.

- (void) refreshPersonalClientInformation
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"CLIENT"];
    request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"stationId" ascending:YES]];
    NSDate *twoHoursAgo = [NSDate dateWithTimeIntervalSinceNow:-2*60*60];
    request.predicate = [NSPredicate predicateWithFormat:@"conditionsUpdated < %@", twoHoursAgo];
    NSError *error = nil;

    int counter = 0;

    // Execute the fetch
    NSArray *matches = [self.managedObjectContext executeFetchRequest:request error:&error];

    // Check what happened in the fetch
    if (!matches) {  // nil means fetch failed;
        NSLog(@"ERROR: Fetch failed to find CLIENT %@ in database", DEFAULT);
    } else if (![matches count]) { // none found, so lets retrieve it below
        return;
    } else {
        for (CLIENT *client in matches) {
            NSDictionary *clientDict = [ClientFetcher clientInfo:client.clientId];

            client.name = [clientDict[NAME] description];

            client.age = [clientDict[AGE] description];
            client.updated = [NSDate date];
            if (![self.managedObjectContext save:&error]) {
                NSLog(@"@@@@@@@@@@@@@@@@@@@@@@@@@@AMRO Unresolved error %@, %@", error, [error userInfo]);
            }

            //Have to rate limit requests at no more than 10 per minute.
            NSLog(@"Sleeping for 6s");
            sleep(6);
            NSLog(@"Done sleeping for 6s");
        }
    }
}

I even tried to run the save in refreshPersonalClientInfo by doing this:

 dispatch_async(dispatch_get_main_queue(), ^{
     [self.managedObjectContext save:nil];
 });

But that did not work, no errors were detected

As an experiment I changed the thread call to this when my view appears and my core data eventually gets saved, the problem is that my view is blocked and I have to wait a long time while the data is getting retrieved. Also when I say eventually, I mean that it does not happen after I call [self.managedContext save:nil], but after a few minutes:

        dispatch_queue_t fetchQ = dispatch_queue_create("Client Fetch", NULL);
        dispatch_async(fetchQ, ^{
            [self.managedObjectContext performBlock:^{
                [self refreshPersonalClientInformation];
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self reload];
                });
            }];
        });

Any suggestions on how I should retrieve my informaion in a background thread and be able to save the data to disk while I still leave my UI accessible?

Upvotes: 0

Views: 1004

Answers (1)

user1330388
user1330388

Reputation:

You're better to create a child context to perform everything in the background:

dispatch_queue_t fetchQ = dispatch_queue_create("Client Fetch", NULL);
    dispatch_async(fetchQ, ^{
        NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        context.parentContext = self.managedObjectContext;
        [context performBlock:^{
            [self refreshPersonalClientInformation];
            dispatch_async(dispatch_get_main_queue(), ^{
                [self reload];
            });
         }];
    });

Upvotes: 1

Related Questions