🧭
Core DataでエラードメインがNSCocoaErrorDomainでエラーコードが133020の場合を再現させて図示したい
はじめに
Core Dataでのデータ更新時にコンフリクトすることがあり、原因は複数あるので整理したい。
この記事ではその原因の1つ、エラーのドメインがNSCocoaErrorDomain
でコードが133020
の場合について再現させるコードを載せておく。
そのため、まず並列実行するがタイミングをずらして順次contextに対する操作を行いマージが成功する例を先に示し、次に並列実行させてマージ元が変更されてしまった例を示す。
TL;DR
-
NSCocoaErrorDomain
の133020
はデータの更新前をマージ元のそれと比較して整合性をチェックしてエラーを出す- 例えば
- 変更前が
default
だとして変更後にA
にするとき、別スレッドでdefault
をB
にしていると、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