PersistenceController initialization container.loadPersistentStores Crashes Thread 0 unsafeMutableAddressor

464 views Asked by At

I've implemented CoreData in production identically to the suggestion by HackingWithSwift: https://www.hackingwithswift.com/quick-start/swiftui/how-to-configure-core-data-to-work-with-swiftui

This is by far the most common crash reason affecting almost 1% of all users. I was reviewing the organizer crash log in a TechTalk without any results. Therefore, respect if any of you knows the cause:

Organizer Crash Log

Here is the specific code used, but as I said, it's as described in the HackingWithSwift article: PersistenceController.init MyApp.init

I appreciate any help, I've been fighting with this for a while, and I assume others must have the same issue since HackingWithSwift is a popular site. Thank you so much!

class GroupedPersistentContainer: NSPersistentContainer {

    enum URLStrings: String {
        case group = "group.app"
    }


    override class func defaultDirectoryURL() -> URL {
        let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: URLStrings.group.rawValue)

        if !FileManager.default.fileExists(atPath: url!.path) {
            try? FileManager.default.createDirectory(at: url!, withIntermediateDirectories: true, attributes: nil)
        }
        return url!
    }
}

Here is an example of how I use core data in prod for background tasks. This function is only for demonstration but the coding style might be the cause of the issue:

func backgroundCoreDataExample(userinfo: UserInfoProperties) {
    Task {
        // some api call or something else
        PersistenceController.shared.container.performBackgroundTask({ context in
            guard let user = User.fetch(viewContext: context) else {
                completion(false)
                return
            }
            user.userid = Int64(userinfo.userid)
            context.saveIfPossible()
            DispatchQueue.main.async {
                let viewContext = PersistenceController.shared.container.viewContext
                if let user = User.fetch(viewContext: viewContext) {
                    print(user.userid)
                }
            }
        })
    }
}

Here is how I am initialising the singleton

@main
struct YourApp: App {
    let persistenceController = PersistenceController.shared
// …
}
2

There are 2 answers

9
Reinhard Männer On

It seems to me that this is a deadlock:
As shown in your stack trace, init(inMemory:) is executed on the main thread, but cannot continue (sqlite3LockAndPrepare), since another thread also runs the init, and locks the database.
Thus I suspect that you are using your singleton PersistenceController.shared also on some background thread that runs concurrently to your main thread.
Maybe you could check this.

EDIT:

It is difficult to make a suggestion without knowing the architecture of your app. Maybe you can do the following:

Initialize the singleton very early in your app, e.g.:

SwiftUI:

@main
struct YourApp: App {
    let persistenceController = PersistenceController.shared
// …
}

UIKit:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    let persistenceController = PersistenceController.shared
// …
}

Start the background threads only after persistenceController has been initialized.

1
lorem ipsum On

Your background function is mixing DispatchQueue and Task, the new "Concurrency" replaces the "old" GCD setup, they should never be mixed.

Concurrency deals directly in terms of "Actors" and GCD uses "Threads".

Apple talks about Core Data and Concurrency in Bring Core Data concurrency to Swift and SwiftUI what you are looking for is around minute 6:50.

It would look something like

func backgroundCoreDataExample(userinfo: UserInfoProperties) async throws -> User {
    let backgroundCtx = PersistenceController.shared.container.newBackgroundContext()
    
    try await backgroundCtx.perform {
        guard let user = User.fetch(viewContext: backgroundCtx) else {
            throw CustomErrors.unableToFetchUser
        }
        user.userid = Int64(userinfo.userid)
        try backCtx.saveIfPossible() // should throw if saving
    }
    
    let viewContext = PersistenceController.shared.container.viewContext
    guard let user = User.fetch(viewContext: viewContext) else {
        throw CustomErrors.unableToFetchUser
    }
    return user
}

Then you can call it on something that uses @MainActor such as a UIViewController or a SwiftUI View or a class or struct that has been marked and is dedicated to updating UI.

let task: Task<Void, Never> = Task { // or .task in a SwiftUI View
    do{
        let user = try await backgroundCoreDataExample(userinfo: userInfo)
    }catch{
        print(error) //Catch and display any errors
    }
}

Concurrency has the advantage that you can easily throw errors all the way to the UI so you can present a user friendly description using error.localizedDescription you can also use task.cancel() to stop the Task mid-operation if needed.

You can learn more with Meet async/await

The CustomError I use looks like this.

enum CustomErrors: LocalizedError {
    case unableToFetchUser
    
    var errorDescription: String?{
        "\(self)".localizedUppercase
    }
}