I ran into problems with my Core Data Unit of Work/Transaction implementation the other day. I was not exercising good NSManagedObjectContext hygiene and end up with conflicts from time to time. Race conditions. Hate ‘em.

The problem is that the parent–child setup of Core Data managed object contexts seems to work just fine for so-called “scratch pad” contexts: create a new child context, perform changes on in, then discard, or save to pass changes to the parent context.

Read operations pose a different challenge, though.

The race conditions I experience stem from this:

#!swift
func someUseCase() {
    let data = objectDataFromDialog()
    unitOfWork().execute {
        createDomainObject(data) // fires notification on success
    }
}

// Behind the scenes, simplified
class UnitOfWork {
    
    func execute(block: () -> Void) {
        
        transactionalContext.performBlock {
            block()
            
            do {
                transactionalContext.save()
            } catch {
                // ...
                return
            }
            
            mainContext.performBlock {
                do {
                    mainContext.save()
                } catch {
                    // ...
                }
            }
        }
    }
}
  • The UnitOfWork saves to transactional and main context after executing the passed block. The transactional context runs on a private/background queue.
  • Domain objects are consequently receiving messages from that block on the background queue of the transaction context.
  • Domain objects fire events/notifications to signal success.
  • Event delivery happens on the main queue. Consumers may be reached before the write operation finishes.

This is my own mistake and can be remedied rather easily. Instead of firing events off from within domain objects, I can enqueue events. The application service which manages the transaction can take care of that:

#!swift
func someUseCase() {
    let eventQueue = EventQueue()
    let data = objectDataFromDialog()
    
    unitOfWork().execute({
        createDomainObject(data, eventQueue)
    }, completion: {
        DomainEventPublisher.publishFromQueue(eventQueue)
    })        
}

Remember that execute runs performBlock on a background queue, so execute will return immediately. That’s why I need another callback.

This way, I will not produce the current race conditions. But then the underlying problem still exists, although it won’t show up for a while: I set up repositories and data stores to the main context.

Fetch operations on a child context seem to pull changes from the persistent store through the main context to the child context. When you mutate objects on the child context while changing something from the main context’s point of view, you’ll end up raising fatal errors.

Also, I should not perform NSFetchRequests outside of the context’s performBlock. The current code does that, though. Wrapping it in a UnitOfWork won’t help – in fact, it makes things worse – because the transactional context is not used to fetch data. It’s just there.

I’m now rethinking the current project setup.

Instead of wiring my custom CoreDataXYZStore objects to the main context during bootstrapping, I should rather create these lightweight objects during transactions with the temporary transactional context.

How do I reach the transactional context which is an implementation detail of the UnitOfWork? I better don’t, since it’s a private detail. I could expose the current context, whichever that is behind the scenes, to the block execute takes. Client code would then look like so:

#!swift
func feedBiggestBanana() {
    let eventQueue = EventQueue()
    
    unitOfWork().execute({ context in
        let bananaRepo = CoreDataBananaRepository(context: context)
        let childRepo = CoreDataChildRepository(context: context)

        let child = childRepo.hungriestChild()
        let banana = bananaRepo.biggestBanana()
    
        child.eat(banana, eventQueue) // enqueue AteFood event
    }, completion: {
        DomainEventPublisher.publishFromQueue(eventQueue)
    })
}

clann Child {
    func eat(food: Food) { ... }
}

Both (highly contrived) fetch operations will be executed on the same context. The likely mutation of child’s belly contents will happen on that same queue. After block execution finishes, the UnitOfWork will save the changes to child in the transactional context. It will then also save changes to the main context. Event delivery is ensured to take place only after everything else is completed.

Is this a solution to my problem?

What about concurrent transactions? I could execute a unit of work 1000 times in a loop, say, and produce concurrent access to 1000 private transactions – and then 1000 save operations on the main context once the transactions finish.

Since performBlock enqueues the block, the 1000 save operations on the main context are not happening concurrently but in sequence on the same queue. That’s useful.

BUT now I access the main context for a fetch request somewhere else in the meantime, not saving changes because I don’t need to. The main context will then not only happen to contain at least the changes from a transaction plus the NSManagedObject I have fetched in the meantime.

I don’t feel well with that, because I don’t want that additional object to clutter up the context and interfere with the transaction. Isn’t that likely to cause trouble when the fetched object happens to be exactly the same that is modified during the transaction?

Could this produce differences I have to merge?

Instead of child contexts, I could create a peer to the main context with the same store coordinator, just like we used to do before parent contexts came to Core Data. Changes to this new context will indeed need to be merged into the main context which is easy thanks to NSManagedObjectContextDidSaveNotification transporting changes in its userInfo dictionary. I don’t know if that causes a different kind of trouble, though.

My hunch at the moment is this rule of thumb:

  • Child contexts work well as scratch pads which can be discarded to insert new entities into the main context.
  • Peer contexts work better for fetches and edit because they keep the main context clean and only dirty themselves, whereas child contexts dirty both themselves and their parent.

Did I miss something here?