Assumptions
I wrote quite a few apps utilising synchronization with CoreData since 2010 and it has never been easy. The architecture I am going to present is trying to meet the following requirements:
- synchronization should work on a background thread to minimise user interface lagging
- synchronization should not interfere with user actions, e.g. a contact should not be updated if user is editing it
- it should be possible to run synchronizations at various priorities
- the architecture should be modular making it easy to extend and modify
- the architecture should not produce merge conflicts between managed object contexts -
NSMergeConflict
Performant core data stack
Two independent managed object contexts should be used to achieve high performance. The first one (main context) will be working with mainQueueConcurrencyType
and the second (background context) with privateQueueConcurrencyType
. To understand that decision you should read the following articles by Florian Kugler: Concurrent Core Data Stacks – Performance Shootout, Backstage with Nested Managed Object Contexts
Draft of the architecture
Data Import Synchronizer manages Data Import Operation Queue. The queue can execute only one Data Import Operation at a time. Each operation saves Background MOC, thereby saving changes to Persistent Store. Once the save is done Background MOC posts NSManagedObjectContextDidSave
. The notification is received by Data Import Synchronizer which calls mergeChanges(fromContextDidSave:)
on Main MOC, in result keeping both context in sync.
There are a few things to keep in mind here.
First, Core Data locks Persistent Store Coordinator for the time save operation is performed by Background MOC. It means that Main MOC will have to wait if it queries Persistent Store Coordinator for some data. It is wise to keep Data Import Operations relatively small.
Second, NSManagedObjectContextDidSave
is not posted synchronously after save()
method call. Technically there is a chance to fetch some managed objects to Main MOC, after Background MOC save()
is executed, and before mergeChanges(fromContextDidSave:)
is performed. It might result in unexpected app behaviour or even crashes.
Third, making changes in Main MOC may result in conflicts with Background MOC. There is no good support for user edits, and this is the issue we are going to resolve next.
Adding support for user edits
Managed objets altered by a user should appear in Main MOC and persistent store. With Data Import Operations executing in the background mergeChanges(fromContextDidSave:)
can be called at any moment, which can result in conflicts. This problem can be avoided for example by pausing Data Import Operation Queue for the time user is performing edits, and this is an approach we are going to focus on.
High level description is the mechanics looks as follows:
- Client asks Data Import Synchronizer for transaction
- Data Import Synchronizer adds Data Import Transaction operation to Data Import Operation Queue with highest possible priority
- Once the operation starts it informs the client that transaction has started
- Data Import Transaction stays in the queue until it is finalised
- The transaction is delivered asynchronously
- Client allows user to perform edits
- Changes are saved to persistent store
- Client finalises the transaction
- Data Import Transaction calls
reset()
on Background MOC - Data Import Transaction is marked as finished and leaves the queue
- Data Import Operations continue their work
- Data Import Transaction calls
As you have probably noticed there are two caveats of this approach:
- one, user need to wait until currently executing Data Import Operation finishes.
- two, instead of merging changes form Main MOC to Background MOC, the latter is reset. I leaned towards this approach as merging changes was not trivial to implement.
The solution worked really well in practice.
- user interface lagging was minimal
- synchronization stopped when user performed edits
- it was possible to assign various priorities to Data Import Operations that needed it
- it was easy to extend synchronization by adding more Data Import Operations
- merge conflicts never occured