wbyoung

Whitney Young is a developer at FadingRed
 

When Mac OS X 10.7 and iOS 5 were released, I was so excited about the new features in Core Data. The additions of explicit concurrency types, nested contexts, and iCloud syncing promised to make developing with Core Data even more delightful. After evaluating these features, our team decided that we were going to use them in the next major release of one of our apps. Unfortunately (as is sometimes the case), some of these features aren’t quite ready for prime time.

Today I’m going to discuss nested context support in Core Data and various issues that exist with them. Some of these issues are minor bugs. Others may be considered functionally correct by the Core Data team, but result in unexpected behavior. Regardless, they add up to nested contexts being a feature you should avoid completely (as of this writing).

Background

So what are nested contexts anyway? To avoid a very lengthy explanation, I’m going to start by assuming that you understand the Core Data stack. Nested contexts allow you to set up a managed object context so that it accesses data from a parent context instead of from a persistent store. If you request an object from a managed object context that has a parent context, Core Data will first look in the parent. If the parent context has that object in memory, you’ll get a new managed object just like that one. So if there are changes in the parent, you’ll get the changed version of the object. If the object doesn’t exist in that context, it will keep going up through parent contexts until it finally fetches the data from the persistent store. Basically, retrieving data goes all the way up through all ancestor contexts. Saving, on the other hand, doesn’t traverse up through parent contexts — it only saves one level up. All of this is pretty well explained elsewhere, so I’m going to leave it at that.

Issues

Throughout the last 9 months, we’ve been using nested contexts in an attempt to simplify and improve some of the data processing that we do in our personal finance application, Koku. It feels like at every turn we’ve been finding issues in Core Data where nested contexts are to blame. Many of these issues are a bit difficult to explain because there is a lot of setup required to reproduce them. With that being said, here is what we’ve found so far:

Child contexts block ancestors

This is one of the most fundamental problems with nested contexts. When I was first told about this through some friends in Chicago, I didn’t believe it. Basically, when you execute a fetch request in a child context, it blocks the parent context while performing the fetch. What does this mean? Well it means that you can’t really use nested contexts to solve a lot of concurrency issues.

Here’s an example: you have a bunch of objects in your database that need to be fetched and have time-intensive code executed on them. The following code looks like it would solve your problems:

NSManagedObjectContext *asyncContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[asyncContext setParentContext:mainContext];
[asyncContext performBlock:^{
    // create fetch request, execute, and do work
}];

But executing that fetch request blocks the main context. Assuming that main context is running on the main thread, then this also makes your application stop responding.

I’ve commonly heard people using this setup for importing from some type of service. And this is where things start to get bad. Let’s say you’re importing a bunch of JSON objects and you need to check if each already exists in Core Data based on an identifier. You write code that loops and checks for an object in Core Data with that identifier, then creates a new object from the JSON data if it doesn’t exist (yes, that could be optimized). It may look like everything is going fine because your UI is still responsive through most of that import (especially if you’re reading from a stream since there will be a delay between each ID check). The truth is, though, that every time you check for an object with an identifier, you’re blocking your UI. It may only be for a fraction of a second, but no one is expecting this.

Now this in itself is not really a large problem. Once you understand it, it’s just another part of the framework that you work with/around. The real problem is that nothing in the documentation indicates that this will happen. In fact, the release notes read:

This pattern has a number of usage scenarios, including:
  • Performing background operations on a second thread or queue.
  • Managing discardable edits, such as in an inspector window or view.

Right there in the release notes, Apple is telling us to use nested contexts to do work in the background. But you absolutely should not use them for that. They will block the parent.

radar://10581934

Making changes into a parent context can be slow

Sometimes the cost of making changes in a child context is outrageous. Simply changing a boolean value on a bunch of objects can be prohibitively slow. It seems that Core Data tries to do something while processing pending changes on the child context. A reasonable assumption is that Core Data is trying to negotiate changes between the two contexts. All we’ve been able to determine is that there’s a lot going on with dictionary lookup that can hang the app. Everything takes far longer than if you were doing the same thing in a context that was tied directly to a persistent store coordinator.

Fetch results are wrong in a child context

It’s possible that fetch requests will not return the right results. I cannot even begin to understand how nested contexts got released when something this fundamental is broken. (Ok, so it’s broken in a very specific way, but still.)

Here’s how you can get Core Data to return the wrong results: set up a nested context relationship just like they describe for UIManagedDocument. For those who are unfamiliar, this involves an asynchronous context that is connected to the persistent store coordinator. The main queue context sets its parent to be this asynchronous context.

With the contexts set up, create a new object. Let’s imagine this object represents a blog and its variable name is awesomeBlog.  Add a couple of articles to that blog. Now if you were to set up a fetch request for all of the articles with a predicate of [NSPredicate predicateWithFormat:@"blog = %@", awesomeBlog], you’d get back the right results. But if you save the main context and then the async context (so the data is actually persisted to the disk) and try executing that fetch request, you would get no results. And it doesn’t matter if you set up the fetch request before or after you persist the data. You’ll never get the right results. Without nested contexts, this works just fine!

There is a workaround for this issue. You can simply obtain a permanent ID for the object when you create it. But there is a catch. The async context (at least in the case of UIManagedDocument) exists so that you can write data asynchronously. By getting a permanent ID for each object you create, you’ve made it so that you’re doing synchronous writes more frequently.

Oh, and as an added bonus, everyone who uses UIManagedDocument will experience this bug.

This has been reported as fixed for an upcoming release of iOS (which means the same should be true for Mac OS X).

radar://11891033 fixed in an upcoming release

Cannot access relationship in a child context for unsaved objects

This is pretty similar to the issue above in that it deals with relationships of objects that have been created. The difference is in the context setup and that it doesn’t really involve saving.

Here’s what can happen: let’s say you’re working in a managed object context called mainContext. It doesn’t need to have a parent context; it can be hooked up directly to the persistent store. Insert a new object into that context. Let’s call it a blog again. Add a few articles to that blog. Now, create a new managed object context and set it’s parent to mainContext. We’ll call this new context editing context. It’s another one of those common workflows that Apple documented in the release notes for nested contexts — discardable edits. But here’s what happens: when you access your blog in editingContext, it has no articles.

The workaround for this is the same as the last one. Just obtain the permanent ID of the object beforehand. This has also been reported as fixed for Mac OS X 10.8 Mountain Lion (which means the same should be true for iOS 6).

radar://10209854 fixed in an upcoming release

Sorting is not honored when there are changes in a parent context

Sort descriptors are important, right? Well they don’t seem to always matter when you’re using nested contexts. If you have changes in a parent context and you execute a fetch request in the child, sorting does not work. Mix that with some of the above issues and you may get out of order, incomplete results. Yikes!

NSFetchedResultsController deadlocks

You never want your application to deadlock. With NSFetchedResultsController and nested contexts, it’s pretty easy to do. Using the same UIManagedDocument setup described above, executing fetch requests in the private queue context while using NSFetchedResultsController with the main queue context will likely deadlock. If you start both at about the same time it happens with almost 100% consistency. NSFetchedResultsController is probably acquiring a lock that it shouldn’t be. This has been reported as fixed for an upcoming release of iOS.

radar://11861499 fixed in an upcoming release

Crash after obtaining permanent IDs

A few of the issues above could be worked around by getting the permanent ID of an object. This can actually lead to a crash in certain circumstances, though. Again using the UIManagedDocument setup, create a new main queue context, editingContext, off of the existing main queue context, mainContext. Create a new object in editingContext. Save editingContext. Obtain a permanent ID for the same object in the mainContext. Core Data will eventually crash when cleaning up resources no longer in use.

radar://10480982 fixed in an upcoming release

Wrapping Up

If you made it through all of those, you’ve probably come to the conclusion that nested contexts are currently something to avoid. If not, the bullet points alone may have been enough to cause some unease. And these are only the issues that we’ve experienced. I wouldn’t be surprised if there are more. In discussions with peers, I’ve found that there are many people who are discovering some of these same issues and finding ways to work around them individually. I hope this list helps you make an informed decision about using nested contexts before you go down the same road we did. Also, please consider filing duplicate radars for those that are listed above. If you have anything to add to this list, please reach out to me on twitter or send me an email.

Thinking back over the lifetime of Core Data, it’s pretty unfortunate to see so many problems with the recently introduced features. I remember back when Core Data was released that SenTestingKit was just making its way into Xcode. At some point, Apple used Core Data as a poster child for unit testing. They claimed they were able to tune and enhance the performance in Core Data, making it screaming fast because of the comprehensive tests that they wrote. Whether this was marketing or not, the framework has been pretty solid throughout the years. The issues that have cropped up with nested contexts (and don’t even get me started on iCloud) feel like huge oversights and problems that fall nicely into the comprehensive test coverage category.

With all that being said, nested contexts are such a wonderful idea that could simplify architecture issues that so many developers using Core Data face. I look forward to the day that they work flawlessly and we get to start using them to make the next generation of data backed applications.

Update 2012.08.08: Updated information on radars. Update 2012.09.16: Updated information on radars.

  1. laomuzhu reblogged this from wbyoung
  2. enstartup reblogged this from wbyoung
  3. wbyoung posted this