Post

CloudKit Zone 刪除的誤解:為什麼我清一個 zone 就把 985 筆資料弄消失

CloudKit Zone 刪除的誤解:為什麼我清一個 zone 就把 985 筆資料弄消失

起因

MeowCare 的 CKShare 陷入 recovery loop,Xcode Console 狂噴:

1
2
3
4
CoreData: fault: Zone metadata is missing it's encoded share data
but is marked for a mutation:
<CKRecordZoneID: zoneName=com.apple.coredata.cloudkit.share.71E08173-55A9-426C-B389-F6D47157E6BB,
 ownerName=__defaultOwner__>

encodedShareAsset = nilneedsShareUpdate = 1。Mirror delegate 一直想修,修不好,container.share 呼叫全部 hang。

我以為這個 share zone 只是壞掉的 metadata,從 CloudKit Console 手動刪掉應該就能讓 mirror 重新走一遍健康流程。

實際動手:Console → Private Database → Data → Zones → 找到那個 com.apple.coredata.cloudkit.share.71E08173-... → 刪除。

結果 app 回報:

1
2
Private — Event: 0, Session: 1
Shared  — Event: 0, Session: 0

985 筆 events 全部消失

為什麼

這是我對 Core Data + CloudKit 共享機制的誤解。正確的模型:

container.share(_:to:) 是「搬移」不是「複製」

一開始,所有 record 住在 owner 的 private database 的 com.apple.coredata.cloudkit.zone(Core Data 自己建的 default custom zone,以下簡稱「主 zone」)。

呼叫 container.share([session], to: nil) 時,NSPersistentCloudKitContainer 做的事:

  1. 建立一個新 zone,名字類似 com.apple.coredata.cloudkit.share.<UUID>(以下簡稱「share zone」)
  2. 把傳入的 session record 從主 zone 搬到 share zone
  3. 把 session relationship 可達的所有 CareEvent record 一起搬到 share zone
  4. 在 share zone 裡放一個 cloudkit.share record(CKShare 本體)
  5. 把 share URL 回傳給我

重點在「搬」。share zone 建立之後,那 985 筆 events 的實體就住在 share zone,主 zone 裡沒了。這是為什麼 partner 接受 share 後,他的 shared database 能讀到這些 records——他的 shared database 就是在 read owner 的 share zone。

刪 share zone = 刪整個 zone 裡的所有 records

CloudKit 的 zone 刪除語意是 cascade:delete zone = delete every record in that zone

我從 Console 刪 share zone 後:

  1. Zone 本身從 CloudKit server 消失
  2. Zone 裡的所有 record(CareSession + 985 CareEvent + CKShare)一併 server-side 刪除
  3. Owner 的本地 SQLite 透過 CloudKit import notification 收到「zone deleted」
  4. Core Data 把對應 record 從本地 store 也刪掉
  5. UI 看到 985 筆變 0 筆

備份 JSON 救了我。

正確的 recovery 流程(如果再遇到)

Zombie share zone 的症狀就真的只是 metadata 有問題,這時候該做的不是刪 zone,而是:

Option A:在 app 裡 unshare

1
2
3
4
5
6
7
8
9
let privateDB = CKContainer(identifier: "...").privateCloudDatabase
let shares = try await container.fetchShares(in: privateStore)
for share in shares {
    // 透過 Core Data 的 API unshare
    try await container.purgeObjectsAndRecordsInZone(
        with: share.recordID.zoneID,
        in: privateStore
    )
}

purgeObjectsAndRecordsInZoneNSPersistentCloudKitContainer 提供的 API,會正確解除 share 並把 record 搬回主 zone(不是刪 zone)。

Option B:用 CKModifyRecordsOperationcloudkit.share record

只刪 CKShare record 本身(它住在 share zone 的根部,system recordName 叫 cloudkit.zoneshare 或 share 的固定 ID),保留 zone 跟裡面的資料 record。

1
2
3
4
5
6
let shareID = CKRecord.ID(recordName: "cloudkit.zoneshare", zoneID: shareZoneID)
let op = CKModifyRecordsOperation(
    recordsToSave: nil,
    recordIDsToDelete: [shareID]
)
privateDB.add(op)

刪完之後,zone 還在、records 還在,但 share 沒了——partner 的 access 失效。然後再重新建新 share。

Option C:換 container identifier

如果真的狀態爛到救不回,最乾淨的就是換 iCloud container(iCloud.your.bundle.v2)。要改:

  1. Apple Developer portal 加新 container
  2. App 的 entitlements 加新 container
  3. Config.containerIdentifier 跟程式碼更新
  4. App 使用者全部從 0 重來(如果 app 還沒多少使用者就無痛)

為什麼 Console 提供刪 zone 按鈕

Console 的 Zones 頁面本來就是給 CloudKit 管理員用的 power tool,刪 zone 在某些情境下是合理動作(例如真的想放棄整個 zone 的所有資料)。錯的是我以為 share zone 只有 metadata、刪掉不會動到業務資料。

正確的心智模型是:

Zone用途資料在哪
_defaultZoneCloudKit 系統 zone沒用 Core Data 時才會放東西
com.apple.coredata.cloudkit.zoneCore Data 主 zone沒 share 的所有資料
com.apple.coredata.cloudkit.share.<UUID>某個 CKShare 的 zone該 share 底下所有 record 的實體

share zone 不是 metadata-only,它裝著完整的資料子樹。

JSON 備份的價值

MeowCare 一開始就做了全資料 JSON export/import 功能,單純是因為我想要「換 app 時能搬家」。沒想到這次救命了。

如果你的 CloudKit app 沒做 JSON export:

  • 做一個吧。Core Data NSManagedObject 可以很輕易包個 Codable 的 Export 結構,JSONEncoder + .iso8601 日期策略,50 行內搞定
  • 使用者也喜歡這個功能,因為他們知道資料可以自己帶走

心得

  1. CloudKit Sharing 的 share zone 不是 metadata-only zone,它是真實資料 zone,刪了就是 cascade delete 所有 record
  2. Console 的 Zones 頁面操作要非常小心,跟刪 S3 bucket 等級的操作
  3. Zombie zone metadata 的正確修法是 unshare(purgeObjectsAndRecordsInZone)或刪 CKShare record,不是刪 zone
  4. JSON export/import 是 CloudKit app 的最後一道防線,做好做滿
  5. 任何「我以為只是 metadata、應該可以放心清」的動作,在 cloud infra 上都要先假設會 cascade,確認完再下手

Apple 的文件對 share zone 的 lifecycle 描述得不夠清楚,這個坑需要自己踩過才知道。希望這篇可以讓下一個人少賠一次資料。

This post is licensed under CC BY 4.0 by the author.