0

I have a NSManagedObject, NewOrder, that I store just the NSManagedObjectID for out of context. When I need to access it, I'm either in the main context, or in a performAndWait() block for a persistentContainer.newBackgroundContext() on a BG thread, and I use context.existingObject(with:NSManagedObjectID) to pull up a context-appropriate instance, and save after making changes.

For some reason, I'm getting old data after making changes. I'm positive that I've saved my changes with try! context.save().

As much as possible, I try to use persistentContainer.newBackgroundContext() instead of the main context so I don't lock up the main thread, which means a good number of contexts touch this NSManagedObjectID variable. The strange thing is, it predictably happens after two newBackgroundContext calls, in the exact same place between one new context and the next. It's almost as if it is recycling a bg context that never was notified of the saved changes.

It looks something like this:

    DispatchQueue.global(qos: .userInitiated).async
    {
        let context = persistentContainer.newBackgroundContext()
        context.performAndWait
        {
            let newOrder = try! context.existingObject(with:self.newOrderOID) as! NewOrder
            let client = try! context.existingObject(with: self.clientOID) as! Client
            print(client.id)//10386
            print(newOrder.userID!)// "10386"

            //replacing old client object with new one, including data on the newOrder object
            context.delete(client)
            let newClient = Client(context: context)
            self.clientOID = newClient.objectID
            print(newClient.id) //10152
            newOrder.userID = String(client.id)
            try! context.save()
            print(newOrder.userID!) //"10152"
        }
    }

    //a bit later in the code, access again to do some reading, NO changes
    let semaphore = DispatchSemaphore(value: 0)
    DispatchQueue.global(qos: .userInitiated).async
    {
        let context = persistentContainer.newBackgroundContext()
        context.performAndWait
        {
            let newOrder = try! context.existingObject(with:self.newOrderOID) as! NewOrder
            let newClient = try! context.existingObject(with: self.clientOID) as! Client
            print(newOrder.userID!)// "10152"
            print(newClient.id) //10152
            semaphore.signal()
        }
    }
    semaphore.wait()
    //IMMEDIATELY AFTER the previous context shows the correct userID
    DispatchQueue.global(qos: .userInitiated).async
    {
        let context = persistentContainer.newBackgroundContext()
        context.performAndWait
        {
            let newOrder = try! context.existingObject(with:self.newOrderOID) as! NewOrder
            let newClient = try! context.existingObject(with: self.clientOID) as! Client
            print(newOrder.userID!)// "10386" ????
            print(newClient.id) //10152 !!
        }
    }

As far as my reading of the documentation has taken me, I'm under the impression that these background contexts are supposed to be rooted directly to the data store, much like the main context is, and should receive updates on every change to the data store. So if I save on one, it should show those changes on all of them. But only SOME of them are reflecting the changes: Sometimes the object won't delete from the store across all contexts, and sometimes the data in the object is old. What's particularly alarming and flummoxing is that one NEW context will have correct data, and another NEW context created after it will have completely different data.

The above example is a rough simplification of something more complicated, so it's possible the above is impossible and there's a mistake in the more complicated code, but I've been fighting this one error for almost a week with no progress except for finding the exact spot it happens, and discovering that the only change between updated data and outdated data is a new background context.

I don't know Objective-C and am about 4 months in on CoreData using Swift, and have run into nothing but trouble trying to teach it to myself. I'm sure I'm missing something here.

Does anyone know why this object would not be updating to all contexts? Am I misunderstanding something?

Changes made since question posted

As per @Jon Rose 's comment, I refactored the entire app to support doing all reading tasks on the main context and all writing tasks in a queue on a private context, and that seemed to remove the issue I was seeing.

Here's the code I added in my CoreData manager (called persistenceManager):

lazy var persistentContainerQueue = OperationQueue()

func enqueueAndSave(shouldWait:Bool = false, closure: @escaping (_ writeContext: NSManagedObjectContext) -> Void)
{
    let semaphore = DispatchSemaphore(value: 0)
    persistentContainerQueue.addOperation()
    {
        self.persistentContainer.performBackgroundTask()
        {writeContext in
            closure(writeContext)
            self.save(writeContext)
            semaphore.signal()
        }
    }
    if shouldWait
    {
        semaphore.wait()
    }
}

How I use it:

persistenceManager.enqueueAndSave(shouldWait: true)
{ writeContext in
    let newOrder = try! writeContext.existingObject(with:self.newOrderOID) as! NewOrder
    let client = try! writeContext.existingObject(with: self.clientOID) as! Client
    print(client.id)//10386
    print(newOrder.userID!)// "10386"

    //replacing old client object with new one, including data on the newOrder object
    writeContext.delete(client)
    let newClient = Client(context: writeContext)
    self.clientOID = newClient.objectID
    print(newClient.id) //10152
    newOrder.userID = String(client.id)
    try! writeContext.save()
    print(newOrder.userID!) //"10152"
}

//a bit later in the code, access again to do some reading, NO changes
persistenceManager.readContext.performAndWait
{
    let newOrder = try! persistenceManager.readContext.existingObject(with:self.newOrderOID) as! NewOrder
    let newClient = try! persistenceManager.readContext.existingObject(with: self.clientOID) as! Client
    print(newOrder.userID!)// "10152"
    print(newClient.id) //10152
}
//IMMEDIATELY AFTER the previous context shows the correct userID, now showing correct data :D

persistenceManager.enqueueAndSave(shouldWait: true)
{ writeContext in
    let newOrder = try! writeContext.existingObject(with:self.newOrderOID) as! NewOrder
    let newClient = try! writeContext.existingObject(with: self.clientOID) as! Client
    print(newOrder.userID!)// "10152"
    print(newClient.id) //10152
}

edit: I still encountered some problems with missing data, which seemed to be solved by putting an NSLock in the persistenceManager that is locked on every save, and unlocked when the save finishes. The getter/setter for every relevant NSManagedObject must also successfully .lock() before returning/writing to their respective variables.

edit: I'm still encountering old data when reading from the viewContext, in other areas of the app. All writes are being queued, and I've turned my viewContext variable into a computed property that will wait until the queue is empty before returning the context.

Basically, I write data on a private context and save, blocking the main thread until the write and save is complete, and then read the data from the same object I just changed by using viewContext.existingObject(with objectID: NSManagedObjectID), and display that data to a UILabel. That data is old data about 80% of the time. In fact, I have a hard time getting it to change, ever. The pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) function is the one that triggers the write.

It doesn't seem to matter whether I use self.persistentContainer.performBackgroundTask()

or

let writeContext = self.persistentContainer.newBackgroundContext() writeContext.performAndWait(){}

I don't understand why writing on a private context to an object with a specific location in the data store, saving, passing the location in the data store to the viewContext, and the using that location to retrieve that data using that data store location would result in any other object besides the updated one. Especially if the changes are queued, and the read of the data waits for all changes to complete before proceeding. It's like the viewContext isn't getting notified of changes made by the write contexts when they save. Both the write contexts and the viewContext are sourced directly in the data store, and documentation indicates they both should be getting notifications of changes. And throughout most of the application, that's exactly what appears to happen. Except for these pickers. (So far anyways. Who knows where a similar issue might pop up again. I have completely lost confidence in the reliability of CoreData.)

It seems the queue improved, but did not solve the problem.

I'm thinking of refactoring the app to find the objects by some unique ID instead of by their objectID, but I have no confidence that will resolve the issues I'm facing.

Any advice would be appreciated.

edit:

I refactored the app, changed every entity to have a property uuid:UUID, and instead of storing the NSManagedObjectID of objects I want to access later or in a different context and using context.existingObject(with:NSManagedObjectID), I store the UUID and do a fetch request based on the UUID. When I do it this way, core data can no longer fulfill the fault for the object once the object is found. The view context thinks there should be an object in the store, but the object is not in the store when it looks. This applies to every newly created object that is created/saved on a private context, when accessed from the viewContext.

A. L. Strine
  • 611
  • 1
  • 7
  • 23
  • Do all your writing in a queue. see a full explanation here: https://stackoverflow.com/questions/42733574/nspersistentcontainer-concurrency-for-saving-to-core-data/42745378#42745378 – Jon Rose Jun 11 '19 at 07:43
  • @Jon Rose I updated the question with the changes you suggested. The queue improved the issue, but it the issue appears to be happening in a different spot now. – A. L. Strine Jun 17 '19 at 19:42

1 Answers1

0

Rolled back the project to using context.existingObject(with:NSManagedObjectID) and the fault fulfilling issue disappeared.

Made all objects called with context.existingObject(with:NSManagedObjectID) computed properties, and called context.refresh(staticObject!, mergeChanges: true) on every get{}, and that fixed the issue with the viewContext having old data in the pickers.

There was still an issue with one of the pickers related to the order of the data not being guaranteed because it was coming from a to-many relationship, which is based in NSSet. A sort every time the data was stored in memory for use by the picker was all that was needed.

Well...I'd like to say the issue is fixed, but I'm spooked because of how many times before I thought it was and it turned out not to be. Tentatively fixed.

Edit: Surprise surprise, it's happening again.

Edit: Finally made an API for accessing and mutating core data variables in a "global" way. Removed context.refresh(staticObject!, mergeChanges: true). Things seem to be working better, I'll update if I still encounter problems.

Edit: The issue with the pickers ended up being more complex than I thought. Basically, I had to first sort with a hash before sorting by any of the properties to ensure that the order would be guaranteed. I later discovered that hash couldn't be from the ObjectID or the object itself, as the hash for these won't be stable. (They include information such as which context they come from, so they're not guaranteed to be the same across contexts.) Instead, I used a hash built from every individual property, which will be the same across contexts.

Edit: Encountered an issue where core data was being efficient and not refreshing objects whose relationship objects had changed, resulting in old data. Refreshing the parent object resolved this issue, incorporated that into my API.

Edit: Solution was still incomplete. Because refresh happened after every access, assignment would be nullified if the variable was accessed again before being saved, so on every set, I'm having the context save. I'm really not worried about performance, I just need this to be a reliable data store.

A. L. Strine
  • 611
  • 1
  • 7
  • 23
  • CoreData was so difficult to work with, I ended up refactoring the app to use SQLite, and built an API around it to make my life...not hell. It's a little slow, especially for heavy operations, but the data is never stale, and with some creative front-end design, the user barely notices the difference. CoreData was not built with beginners and numbskulls in mind. No peasants like myself allowed, only the royal cadre of the elders of the internet. – A. L. Strine Dec 08 '21 at 16:51