Using NSManagedObjectContext background context with actors

Does anybody use Core Data together with actors? I have a service that fetches data from remote server and populates it into Core Data. It felt natural to make this service into an actor with async functions, but I am getting compiler warnings when using NSManagedObjectContext background context

Sending 'self.backgroundContext' risks causing data races; this is an error in the Swift 6 language mode

actor MyService {
    let api: RemoteServerApi
    let backgroundContext: NSManagedObjectContext

    init(api: RemoteServerApi, backgroundContext: NSManagedObjectContext) {
        self.api = api
        self.backgroundContext = backgroundContext
    }

    private func perform<CoreDataObject: NSManagedObject, T: Sendable>(
        with objectId: NSManagedObjectID,
        type: CoreDataObject.Type,
        _ closure: @escaping @Sendable (CoreDataObject, NSManagedObjectContext) throws -> T
    ) async throws -> T {
        try backgroundContext.performAndWait { // no warnings here
            let object = self.backgroundContext.object(with: objectId)
            assert(object.entity == type.entity())
            let coreDataObject = object as! CoreDataObject
            return try closure(coreDataObject, self.backgroundContext)
        }
    }

    private func perform2<CoreDataObject: NSManagedObject, T: Sendable>(
        with objectId: NSManagedObjectID,
        type: CoreDataObject.Type,
        _ closure: @escaping @Sendable (CoreDataObject, NSManagedObjectContext) throws -> T
    ) async throws -> T {
        try await backgroundContext.perform(schedule: .immediate) { // warning: Sending 'self.backgroundContext' risks causing data races; this is an error in the Swift 6 language mode
            let object = self.backgroundContext.object(with: objectId)
            assert(object.entity == type.entity())
            let coreDataObject = object as! CoreDataObject
            return try closure(coreDataObject, self.backgroundContext)
        }
    }

    func fetchFeed(feedObjectId: NSManagedObjectID) async throws {
        let feedId = try await perform(with: feedObjectId, type: MyFeed.self) { feed, _ in
            feed.id
        }

        let posts = try await api.fetchPosts(feedId)

        try await perform(with: feedObjectId, type: MyFeed.self) { feed, context in
//            feed.posts = posts
            try context.save()
        }
    }
}

Am I doing something obviously wrong?

If it helps, I think that the first MyService's perform() doesn't emit errors because here:

try backgroundContext.performAndWait {
    // Nonisolated context!
    let object = self.backgroundContext.object(with: objectId)
    ...
    return try closure(coreDataObject, self.backgroundContext)
}

By using the synchronous performAndWait you ensure that backgroundContext, which is a variable protected by MyService's actor isolation context, can be sent safely to the (synchronous) closure because nothing else can be executed concurrently (on the actor) to modify said backgroundContext.

However in MyService's perform2(), when you do this:

try await backgroundContext.perform(schedule: .immediate) {
    // Another nonisolated context!
    let object = self.backgroundContext.object(with: objectId)
    ...
    return try closure(coreDataObject, self.backgroundContext)
}

You introduce a suspension point which means that the actor is free to execute other actor isolated methods (which could potentially modify backgroundContext) concurrently with the execution of the perform { ... } closure, which could also modify backgroundContext.

1 Like