New NSPersistentCloudKitContainer crash on iOS14

As soon as I shipped a new version of Percento with iOS14 support, I noticed a new top crasher:

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Triggered by Thread:  5

Last Exception Backtrace:
0   CoreFoundation                	0x1995525ac __exceptionPreprocess + 220 (NSException.m:199)
1   libobjc.A.dylib               	0x1ad5cc42c objc_exception_throw + 60 (objc-exception.mm:565)
2   CloudKit                      	0x19f3734fc -[CKScheduler _submitActivity:withCriteria:] + 520 (CKScheduler.m:145)
3   CloudKit                      	0x19f377020 -[CKScheduler getDeviceCountForActivity:completionHandler:] + 432 (CKScheduler.m:561)
4   CloudKit                      	0x19f375c20 -[CKScheduler getSuggestedXPCActivityCriteriaForActivity:completionHandler:] + 960 (CKScheduler.m:371)
5   CloudKit                      	0x19f373204 -[CKScheduler submitActivity:] + 332 (CKScheduler.m:91)
6   CoreData                      	0x19f727cc0 __65-[NSCloudKitMirroringDelegate checkAndScheduleImportIfNecessary:]_block_invoke_2 + 352 (NSCloudKitMirroringDelegate.m:2305)
7   CoreData                      	0x19f618410 developerSubmittedBlockToNSManagedObjectContextPerform + 164 (NSManagedObjectContext.m:3880)
8   libdispatch.dylib             	0x199150ac8 _dispatch_client_callout + 20 (object.m:559)
9   libdispatch.dylib             	0x19915ec8c _dispatch_lane_barrier_sync_invoke_and_complete + 60 (queue.c:998)
10  CoreData                      	0x19f4e5b9c -[NSManagedObjectContext performBlockAndWait:] + 268 (NSManagedObjectContext.m:3997)
11  CoreData                      	0x19f727af0 __65-[NSCloudKitMirroringDelegate checkAndScheduleImportIfNecessary:]_block_invoke + 272 (NSCloudKitMirroringDelegate.m:2260)
12  CoreData                      	0x19f777704 -[PFCloudKitStoreMonitor performBlock:] + 116 (PFCloudKitStoreMonitor.m:140)
13  CoreData                      	0x19f71f0e8 -[NSCloudKitMirroringDelegate checkAndScheduleImportIfNecessary:] + 204 (NSCloudKitMirroringDelegate.m:2255)
14  CoreData                      	0x19f721824 -[NSCloudKitMirroringDelegate _importFinishedWithResult:importer:] + 312 (NSCloudKitMirroringDelegate.m:1041)
15  CoreData                      	0x19f721684 __57-[NSCloudKitMirroringDelegate _performImportWithRequest:]_block_invoke_2 + 256 (NSCloudKitMirroringDelegate.m:1004)
16  CoreData                      	0x19f680250 __70-[PFCloudKitImporter databaseFetchFinishWithContext:error:completion:]_block_invoke + 1000 (PFCloudKitImporter.m:275)
17  CoreData                      	0x19f777704 -[PFCloudKitStoreMonitor performBlock:] + 116 (PFCloudKitStoreMonitor.m:140)
18  CoreData                      	0x19f67fe4c -[PFCloudKitImporter databaseFetchFinishWithContext:error:completion:] + 344 (PFCloudKitImporter.m:216)
19  CoreData                      	0x19f67fc50 __54-[PFCloudKitImporter importIfNecessaryWithCompletion:]_block_invoke_2.83 + 52 (PFCloudKitImporter.m:133)
20  libdispatch.dylib             	0x19914efd0 _dispatch_call_block_and_release + 32 (init.c:1454)
21  libdispatch.dylib             	0x199150ac8 _dispatch_client_callout + 20 (object.m:559)
22  libdispatch.dylib             	0x199157c08 _dispatch_lane_serial_drain + 580 (inline_internal.h:2548)
23  libdispatch.dylib             	0x199158768 _dispatch_lane_invoke + 460 (queue.c:3862)
24  libdispatch.dylib             	0x199162528 _dispatch_workloop_worker_thread + 708 (queue.c:6590)
25  libsystem_pthread.dylib       	0x1e0aac908 _pthread_wqthread + 276 (pthread.c:2194)
26  libsystem_pthread.dylib       	0x1e0ab377c start_wqthread + 8

The result of Googling these classes and functions are completely blank. Fortunately searching in Apple forum leads me to this discussion.

I then realized it could be a concurrency issue with NSPersistentCloudKitContainer.

In Percento, NSPersistentCloudKitContainer is recreated multiple times if the user switch language, currency or theme.

func softBootApp() {
    // NSPersistentCloudKitContainer is initialized when DataEngine is initialized
    self.engine = DataEngine(cloudKitEnabled: true, completionClosure: { engine in
        self.setupRootVC(viewContext: engine.viewContext, backgroundContext: engine.backgroundContext)
    })
}

From the log, an exception Must register a handler for activity identifier is thrown when NSCloudKitMirroringDelegate is Told to tear down because the store has been removed. This is new to iOS14.

CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate tearDown]_block_invoke(608): <NSCloudKitMirroringDelegate: 0x2811cb8e0>: Told to tear down because the store has been removed.
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _enqueueRequest:]_block_invoke(755): <NSCloudKitMirroringDelegate: 0x2811f3260>: enqueuing request: <NSCloudKitMirroringExportRequest: 0x282423870> 7B57EBEF-CB1B-4D8B-AD7E-09FA6F423534
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _enqueueRequest:]_block_invoke_2(764): Enqueued request: <NSCloudKitMirroringExportRequest: 0x282423870> 7B57EBEF-CB1B-4D8B-AD7E-09FA6F423534
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest](2369): <NSCloudKitMirroringDelegate: 0x2811f3260>: Checking for pending requests.
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _enqueueRequest:]_block_invoke(755): <NSCloudKitMirroringDelegate: 0x2811f3260>: enqueuing request: <NSCloudKitMirroringImportRequest: 0x282422220> 5DA46EB6-AC5A-49F7-A537-32A7D9B9952A
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _enqueueRequest:]_block_invoke_2(764): Enqueued request: <NSCloudKitMirroringImportRequest: 0x282422220> 5DA46EB6-AC5A-49F7-A537-32A7D9B9952A
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest](2369): <NSCloudKitMirroringDelegate: 0x2811f3260>: Checking for pending requests.
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest]_block_invoke(2382): <NSCloudKitMirroringDelegate: 0x2811f3260>: Executing: <NSCloudKitMirroringImportRequest: 0x282422220> 5DA46EB6-AC5A-49F7-A537-32A7D9B9952A
CoreData: CloudKit: CoreData+CloudKit: -[NSCloudKitMirroringDelegate checkAndExecuteNextRequest]_block_invoke(2378): <NSCloudKitMirroringDelegate: 0x2811f3260>: Deferring additional work. There is still an active request: <NSCloudKitMirroringImportRequest: 0x282422220> 5DA46EB6-AC5A-49F7-A537-32A7D9B9952A
CoreData: debug: CoreData+CloudKit: -[NSCloudKitMirroringDelegate managedObjectContextSaved:](2057): <NSCloudKitMirroringDelegate: 0x2811f3260>: Observed context save: <NSPersistentStoreCoordinator: 0x280188230> - <NSManagedObjectContext: 0x281199c70>
CoreData: CloudKit: CoreData+CloudKit: -[PFCloudKitImporter processWorkItemsWithCompletion:](340): <PFCloudKitImporter: 0x283008880>: Processing work items: (
    "<PFCloudKitImporterZoneChangedWorkItem: 0x281be5680 - <NSCloudKitMirroringImportRequest: 0x282422220> 5DA46EB6-AC5A-49F7-A537-32A7D9B9952A> {\n(\n    \"<CKRecordZoneID: 0x282a9b940; ownerName=__defaultOwner__, zoneName=com.apple.coredata.cloudkit.zone>\"\n)\n}"
)
CoreData: debug: CoreData+CloudKit: -[NSCloudKitMirroringDelegate managedObjectContextSaved:](2057): <NSCloudKitMirroringDelegate: 0x2811f3260>: Observed context save: <NSPersistentStoreCoordinator: 0x280188230> - <NSManagedObjectContext: 0x2811c2e50>
2020-10-05 11:51:21.018978+0800 Percento[20667:2107584] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Must register a handler for activity identifier "com.apple.coredata.cloudkit.activity.export.16BBC6CD-C14D-4A39-8FB8-828405E3F250" before submitting it'
*** First throw call stack:
(0x1a063e5ac 0x1b46b842c 0x1a645f4fc 0x1a6463020 0x1a6461c20 0x1a645f204 0x1a6812ccc 0x1a6812c1c 0x1a6863704 0x1a68129a0 0x1a680b944 0x105e2bb68 0x105e2d5f0 0x105e34fa8 0x105e35ce8 0x105e41e38 0x1e7b90908 0x1e7b9777c)
libc++abi.dylib: terminating with uncaught exception of type CKException
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Must register a handler for activity identifier "com.apple.coredata.cloudkit.activity.export.16BBC6CD-C14D-4A39-8FB8-828405E3F250" before submitting it'
terminating with uncaught exception of type CKException

The solution is easy: For my case, there is no reason to recreate NSPersistentCloudKitContainer multiple times, so I hold the instance in AppDelegate and reuse when used in softBootApp.

Posted 2020-10-06

More writing at jakehao.com