Occasional crash on `context.save()`

Recently, I noticed many crashes reported daily for context.save() in my code. I tried to resolve the issue by refactoring some of the code. My key change is to use context.performAndWait to fetch Core Data objects instead of passing them around.

I refactored the code to pass NSManagedObjectID around instead of NSManagedObject itself because NSManagedObjectID is thread-safe.

However, even after the refactor, the issue is still. The crash log is attached as below.

I'm confused that it doesn't crash on all devices so I have no clue to find the pattern of the crashes. I also enabled -com.apple.CoreData.ConcurrencyDebug 1 on my local dev box, but seems the error cannot be caught.

The new code (after I refactored) still causes the crash:

    private func update(movements: [InferredMovement], from visitOutObjectId: NSManagedObjectID, to visitInObjectId: NSManagedObjectID?) {
        let context = PersistenceController.shared.container.viewContext
        context.performAndWait {
            guard let visitOut = try? context.existingObject(with: visitOutObjectId) as? Visit else {
                return
            }

            var visitIn: Visit?
            if let visitInObjectId {
                visitIn = try? context.existingObject(with: visitInObjectId) as? Visit
            }

            visitOut.movementsOut.forEach { context.delete($0) }
            visitIn?.movementsIn.forEach { context.delete($0) }

            for inferred in movements {
                let movement = Movement(context: context)
                movement.type = inferred.type
                movement.interval = inferred.interval
                movement.visitFrom_ = visitOut
                movement.visitTo_ = visitIn
            }

            visitOut.objectWillChange.send()
            context.saveIfNeeded()
        }
    }

Note, saveIfNeeded() is an extension function implemented as:

extension NSManagedObjectContext {
    
    /// Only performs a save if there are changes to commit.
    @discardableResult
    public func saveIfNeeded() -> Error? {
        guard hasChanges else {
            return nil
        }
        do {
            try save()
            return nil
        } catch {
            defaultLogger.error("Core Data Error: Failed to save context")
            return error
        }
    }
}

Answered by DTS Engineer in 791117022

The code snippet looks safe to me, except that visitOut.objectWillChange.send() may have the observers do something bad on the objects, which isn't shown in the snippet. You might carefully review that part.

Providing a full crash report may help as well, as it will probably unveil more information. Also, Core Data typically logs more information when hitting an exception, which goes to the sysdiagnose. If you can gather and analyze a sysdiagnose from the device that just reproduced the issue, you will likely find the information. To capture a sysdiagnose, see Profile and Logs.

The part of the crash report in your post, specifically the -[NSObject(NSObject) doesNotRecognizeSelector:] frame triggered by [NSManagedObjectContext save:], indicates an object that isn't of the expected type, which can be:

a. A managed object (NSManagedObject) that was accessed from a wrong queue. Core Data requires that accessing a managed object must be done from the queue of the managed context (NSManagedObjectContext). Failing to follow the rule may trigger a random crash like this.

b. A zombie object, which might be something referenced by a managed object and was over-released somewhere.

You can avoid #a by following this pattern:

  1. Wrap the code accessing the context and its objects with perform(_:) or performAndWait(_:).

  2. If you need to pass Core Data objects across different contexts, use NSManagedObjectID, rather than NSManagedObject.

To better understand the Core Data concurrency topic, I recommend you to go through the following technical resources:

You have used -com.apple.CoreData.ConcurrencyDebug 1 as a launch argument to check if your code has the concurrency issue, and so you are on the right track. Be sure to exercise all the code paths though. If you turn the argument on, reproduce the issue, and don't see that the debugger halts at the following symbol: +[NSManagedObjectContext __Multithreading_Violation_AllThatIsLeftToUsIsHonor__], you should be fine.

For #b, you can investigate zombie objects in your app by following the guidance in Investigating Crashes for Zombie Objects. After triggering the problem by using the Zombies instrument, you can track down the problem by using the information provided by the instrument.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

The code snippet looks safe to me, except that visitOut.objectWillChange.send() may have the observers do something bad on the objects, which isn't shown in the snippet. You might carefully review that part.

Providing a full crash report may help as well, as it will probably unveil more information. Also, Core Data typically logs more information when hitting an exception, which goes to the sysdiagnose. If you can gather and analyze a sysdiagnose from the device that just reproduced the issue, you will likely find the information. To capture a sysdiagnose, see Profile and Logs.

The part of the crash report in your post, specifically the -[NSObject(NSObject) doesNotRecognizeSelector:] frame triggered by [NSManagedObjectContext save:], indicates an object that isn't of the expected type, which can be:

a. A managed object (NSManagedObject) that was accessed from a wrong queue. Core Data requires that accessing a managed object must be done from the queue of the managed context (NSManagedObjectContext). Failing to follow the rule may trigger a random crash like this.

b. A zombie object, which might be something referenced by a managed object and was over-released somewhere.

You can avoid #a by following this pattern:

  1. Wrap the code accessing the context and its objects with perform(_:) or performAndWait(_:).

  2. If you need to pass Core Data objects across different contexts, use NSManagedObjectID, rather than NSManagedObject.

To better understand the Core Data concurrency topic, I recommend you to go through the following technical resources:

You have used -com.apple.CoreData.ConcurrencyDebug 1 as a launch argument to check if your code has the concurrency issue, and so you are on the right track. Be sure to exercise all the code paths though. If you turn the argument on, reproduce the issue, and don't see that the debugger halts at the following symbol: +[NSManagedObjectContext __Multithreading_Violation_AllThatIsLeftToUsIsHonor__], you should be fine.

For #b, you can investigate zombie objects in your app by following the guidance in Investigating Crashes for Zombie Objects. After triggering the problem by using the Zombies instrument, you can track down the problem by using the information provided by the instrument.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Occasional crash on `context.save()`
 
 
Q