How should I guarantee fetch results from a different thread in a nested contexts are up to date, when saves are done asynchronously in background?
Asked Answered
B

2

3

I've read the following Behavior differences between performBlock: and performBlockAndWait:? But wasn't able to find an answer to my question.

The following code is picked up from an RayWenderlich video. Specifically at 10:05 the code is something like this:

class CoreDataStack {
    var coordinator : NSPersistentStoreCoordinator

    init(coordinator: NSPersistentStoreCoordinator){
        self.coordinator = coordinator
    }
    // private, parent, in background used for saving
    private lazy var savingContext : NSManagedObjectContext = {
        let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        moc.persistentStoreCoordinator = coordinator
        return moc
    }()

    lazy var mainManagedObjectedContext : NSManagedObjectContext = {
        let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        moc.parent = self.savingContext
        return moc
    }()

    func saveMainContext() {
        guard savingContext.hasChanges || mainManagedObjectedContext.hasChanges else {
            return
        }

        mainManagedObjectedContext.performAndWait {
            do {
                try mainManagedObjectedContext.save()
            }catch let error{
                fatalError(error.localizedDescription)
            }
        }

        savingContext.perform {
            do {
                try self.savingContext.save()
            }catch let error{
                fatalError(error.localizedDescription)
            }
        }
    }
}

From what I understand what happens is that the main context just passes the changes to its parent context which is a private, background context. It does this synchronously.

Then the parent, private context, does the actual saving against sqlite in a background thread asynchronously. Long story short this helps us a lot with performance. But what about data integrity?!

Imagine if I was to do this:

let coredataManager = CoreDataStack()
coredataManager.saveMainContext() // save is done asynchronously in background queue
coredataManager.mainManagedObjectedContext.fetch(fetchrequest) 

How can I guarantee that my fetch is reading the most recent and updated results?

If we do our writes asynchronously then isn't there a chance that another read at the same time could end up with unexpected results ie results of the save changes could or could not be there?

EDIT: I've made an improvement with the code below. I can make my save take in a completionHandler parameter. But that doesn't resolve the entire problem. What if I'm making a fetchRequest from a mainQueue somewhere else that isn't aware that a save is happening at the same time?

enum SaveStatus{
    case noChanges
    case failure
    case success
}


func saveMainContext(completionHandler: (SaveStatus -> ())) {
    guard savingContext.hasChanges || mainManagedObjectedContext.hasChanges else {
        completionHandler(.noChanges)
        return
    }

    mainManagedObjectedContext.performAndWait {
        do {
            try mainManagedObjectedContext.save()
        }catch let error{
            completionHandler(.failure)
            fatalError(error.localizedDescription)
        }
    }

    savingContext.perform {
        do {
            try self.savingContext.save()
            completionHandler(.succes)
        }catch let error{
            completionHandler(.failure)
            fatalError(error.localizedDescription)
        }
    }
}
Blancheblanchette answered 31/12, 2018 at 20:32 Comment(1)
You should probably not use fatalError() in case of failure to save as it will crash the app. When saveContext failed, you're almost guaranteed to have unsaved data, and a crash will guarantee that unsaved data is lost.Your idea with the SaveStatus enum is good, but completionHandler should, if possible, do something intelligent with information about failure to save. This is a very interesting problem, and one that is difficult to solve for complex apps.Northbound
B
0

The question isn't specific to core-data.

It's the classic read-write question.

The common approach with protecting a datasource is to access your datasource using a serial queue. Otherwise yeah without the serial queue you will have a read-write problem.

In the following example:

let coredataManager = CoreDataStack() // 1
coredataManager.saveMainContext() // 2 save is done asynchronously in background queue
coredataManager.mainManagedObjectedContext.fetch(fetchrequest) // 3

coredataManager is to be accessed from a serial queue. So even if the write in the 2nd line is done asynchronously, the read at line 3, will have to wait until the serial queue is unblocked.

Blancheblanchette answered 22/1, 2020 at 17:28 Comment(0)
B
0

All calls to mainManagedObjectContext will be synchronous and therefore blocking. If you call saveMainContext() and immediately afterwards call mainManagedObjectedContext.fetch(fetchrequest), the fetch request will not go through until the save request is completed, even if the save/fetch requests come from different queues (see the paragraph on FIFO in your link above).

When you perform a fetch request, you aren't pulling from the persistent storage - you're pulling from the child container, whom you just updated. You don't need to wait for the changes to be committed to the persistent storage, since you aren't accessing the data from there. The child container will give you the latest changes.

The child container is a container - it will hold your latest changes in memory (as opposed to stored on the disk - that is the persistent container's job).

The real issue here is that your CoreDataStack should implement the singleton pattern to prevent instantiating multiple versions of the same containers (that would still technically be on the same thread and therefore serialized, but accessing the containers wouldn't be thread safe). In other words, each time you instantiate CoreDataStack(), you're creating a new savingContext and mainManagedObjectedContext.

Instead, instantiate it just once.

class CoreDataStack {

    var coordinator: NSPersistentStoreCoordinator

    public static let sharedInstance = CoreDataStack()

    private override init() {
        self.coordinator = NSPersistantStoreCoordinator()
    }

    ...
    rest of your code here
    ...
}

And call like this:

CoreDataStack.sharedInstance.saveMainContext()

(See this link re: 'does the child have the same objects as the parent?')

The only case where a child would not be synced up with the parent is where you have multiple children accessing the same parent - but that doesn't seem to be the case here.

Borries answered 5/6, 2019 at 21:2 Comment(0)
B
0

The question isn't specific to core-data.

It's the classic read-write question.

The common approach with protecting a datasource is to access your datasource using a serial queue. Otherwise yeah without the serial queue you will have a read-write problem.

In the following example:

let coredataManager = CoreDataStack() // 1
coredataManager.saveMainContext() // 2 save is done asynchronously in background queue
coredataManager.mainManagedObjectedContext.fetch(fetchrequest) // 3

coredataManager is to be accessed from a serial queue. So even if the write in the 2nd line is done asynchronously, the read at line 3, will have to wait until the serial queue is unblocked.

Blancheblanchette answered 22/1, 2020 at 17:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.