🧭

Core DataでエラードメインがNSCocoaErrorDomainでエラーコードが133020の場合を再現させて図示したい

2022/07/03に公開

はじめに

Core Dataでのデータ更新時にコンフリクトすることがあり、原因は複数あるので整理したい。

この記事ではその原因の1つ、エラーのドメインがNSCocoaErrorDomainでコードが133020の場合について再現させるコードを載せておく。

そのため、まず並列実行するがタイミングをずらして順次contextに対する操作を行いマージが成功する例を先に示し、次に並列実行させてマージ元が変更されてしまった例を示す。

TL;DR

  • NSCocoaErrorDomain133020はデータの更新前をマージ元のそれと比較して整合性をチェックしてエラーを出す
    • 例えば
      • 変更前がdefaultだとして変更後にAにするとき、別スレッドでdefaultBにしていると、Aにした瞬間にコンフリクトする
        • もちろんこれはマージポリシーで挙動を変えられる

(マージポリシーでエラーが起こらないようにすることは簡単だが、それは眠っている問題を解決していないのでここでは設定を変更しない)

前提

  • マージポリシーはNSErrorMergePolicyとし不整合があればエラーとする
  • NSManagedObjectとしてPersonクラスを定義しておく
  • PersonをCore Dataで新規作成しておく
  • 実験前にはCore Dataから削除
  • ワーカースレッドを2つ用意してそれぞれ同時にContextを作成する
extension Person {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Person> {
        return NSFetchRequest<Person>(entityName: "Person")
    }
    @NSManaged public var id: Int
    // 今回はこのnameを上書きする
    @NSManaged public var name: String?
}

実験

全て削除できるようにしておく

    func deleteAll() {
        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Person")
        let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)

        do {
            try persistentContainer.persistentStoreCoordinator.execute(deleteRequest, with: persistentContainer.viewContext)
        } catch {
            print(error)
        }
    }

成功する時

実際のコード

    func createPerson0() {
        deleteAll()

        do {
            // 新規作成
            let context = persistentContainer.newBackgroundContext()
            context.mergePolicy = NSErrorMergePolicy

            let person = Person(context: context)
            person.id = 1
            person.name = "default"

            if context.hasChanges {
                try context.save()
            }
        } catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }

        // 更新A
        DispatchQueue.global().async {
            print("Task A begin ---")
            let context = self.persistentContainer.newBackgroundContext()
            context.mergePolicy = NSErrorMergePolicy
            context.undoManager = nil

            let request = Person.fetchRequest()
            request.fetchLimit = 1
            let person = try! context.fetch(request).first!
            print(person.objectID)
            print("Task A Update")
            person.name = "A1"

            if context.hasChanges {
                do {
                    print("Task A save")
                    try context.save()
                    print("Task A end ---")
                } catch {
                    let nserror = error as NSError
                    fatalError("🔞Unresolved error \(nserror), \(nserror.userInfo)")
                }
            }
        }

        // 更新B
        DispatchQueue.global().async {
            print("Task B sleep begin")
            Thread.sleep(forTimeInterval: 1)
            print("Task B sleep end")

            print("Task B begin ---")
            let context = self.persistentContainer.newBackgroundContext()
            context.mergePolicy = NSErrorMergePolicy
            context.undoManager = nil

            let request = Person.fetchRequest()
            request.fetchLimit = 1
            let person = try! context.fetch(request).first!
            print(person.objectID)
            print("Task B Update")
            person.name = "B1"

            if context.hasChanges {
                do {
                    print("Task B save")
                    try context.save()
                    print("Task B end ---")
                } catch {
                    let nserror = error as NSError
                    fatalError("😂Unresolved error \(nserror), \(nserror.userInfo)")
                }
            }
        }
    }

出力は以下

Task A begin ---
Task B sleep begin
0x8b81bbf466b2236e <x-coredata://437D8408-28DF-42E5-90A6-A57F3404DA9A/Person/p68>
Task A Update
Task A save
Task A end ---
Task B sleep end
Task B begin ---
0x8b81bbf466b2236e <x-coredata://437D8408-28DF-42E5-90A6-A57F3404DA9A/Person/p68>
Task B Update
Task B save
Task B end ---

失敗する時

実際のコード


    // Error Domain=NSCocoaErrorDomain Code=133020
    func createPerson0() {
        deleteAll()

        do {
            // 新規作成
            let context = persistentContainer.newBackgroundContext()
            context.mergePolicy = NSErrorMergePolicy

            let person = Person(context: context)
            person.id = 1
            person.name = "default"

            if context.hasChanges {
                try context.save()
            }
        } catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }

        // 更新A
        DispatchQueue.global().async {
            print("Task A begin ---")
            let context = self.persistentContainer.newBackgroundContext()
            context.mergePolicy = NSErrorMergePolicy
            context.undoManager = nil

            let request = Person.fetchRequest()
            request.fetchLimit = 1
            let person = try! context.fetch(request).first!
            print(person.objectID)
            print("Task A Update")
            person.name = "A1"

            print("Task A sleep begin")
            Thread.sleep(forTimeInterval: 1)
            print("Task A sleep end")

            if context.hasChanges {
                do {
                    print("Task A save")
                    try context.save()
                    print("Task A end ---")
                } catch {
                    let nserror = error as NSError
                    fatalError("🔞Unresolved error \(nserror), \(nserror.userInfo)")
                }
            }
        }

        // 更新B
        DispatchQueue.global().async {
            print("Task B begin ---")
            let context = self.persistentContainer.newBackgroundContext()
            context.mergePolicy = NSErrorMergePolicy
            context.undoManager = nil

            let request = Person.fetchRequest()
            request.fetchLimit = 1
            let person = try! context.fetch(request).first!
            print(person.objectID)
            print("Task B Update")
            person.name = "B1"

            if context.hasChanges {
                do {
                    print("Task B save")
                    try context.save()
                    print("Task B end ---")
                } catch {
                    let nserror = error as NSError
                    fatalError("😂Unresolved error \(nserror), \(nserror.userInfo)")
                }
            }
        }
    }
Task A begin ---
Task B begin ---
0x83265859e9f76ce2 <x-coredata://437D8408-28DF-42E5-90A6-A57F3404DA9A/Person/p70>
Task A Update
0x83265859e9f76ce2 <x-coredata://437D8408-28DF-42E5-90A6-A57F3404DA9A/Person/p70>
Task B Update
Task A sleep begin
Task B save
Task B end ---
Task A sleep end
Task A save
CoreDataCreateOrUpdate/AppDelegate.swift:154: Fatal error: 🔞Unresolved error Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
    "NSMergeConflict (0x6000015aaf80) for NSManagedObject (0x6000023ca080) with objectID '0x83265859e9f76ce2 <x-coredata://437D8408-28DF-42E5-90A6-A57F3404DA9A/Person/p70>' with oldVersion = 1 and newVersion = 2 and old object snapshot = {\n    id = 1;\n    name = default;\n} and new cached row = {\n    id = 1;\n    name = B1;\n}"
), NSExceptionOmitCallstacks=true}, ["NSExceptionOmitCallstacks": 1, "conflictList": <__NSArrayM 0x600000ebf1b0>(
NSMergeConflict (0x6000015aaf80) for NSManagedObject (0x6000023ca080) with objectID '0x83265859e9f76ce2 <x-coredata://437D8408-28DF-42E5-90A6-A57F3404DA9A/Person/p70>' with oldVersion = 1 and newVersion = 2 and old object snapshot = {
    id = 1;
    name = default;
} and new cached row = {
    id = 1;
    name = B1;
}
)

上記再現のためにThread.sleepを使っているがこれは更新タイミングを制御したいからであってプロダクションコードでこんなのやってはいけない。念の為。

その他

さらにDispatchQueue.global().asyncを使ったコードをTask.detachedでやってみたが2つのコンテキストが順次作成されシリアルに実行され成功してしまった。Task.detachedを2つつくるとそれぞれ独立してくれるかと思いきやそうでもないのか...。

エラーコードが133021の場合も存在する。その場合はスレッド関係なく整合性がとれていない場合発生する。もちろんワーカースレッドにより整合性がとれない場合もある。

このコンフリクトを解決するには

  • マージポリシーを設定する
    • しかしマージポリシーで解決するのは本当の問題を解決してない気がする
  • 書き込みは常にシリアルに実行する
    • やり方
      • OperationQueueでqueueの数を1にして常にそれを使う
      • actorを設計しそれが更新するようにする

Discussion