Reputation: 376
So I'm having a really weird problem, I suppose I either dont understand how CloudKit works under the hood or I encountered a bug in CloudKit.
So, the issue looks like this:
App initial state:
I have 5 "Package" records, lets call them A, B, C, D, E.
User action
The user will delete "Package" record E and at some later point in time he will press a refresh button which will fetch all current "Package" records from the cloud.
The problem
When the user presses the refresh button, the app will basically look at the existing locally stored "Package" records, and will create a CKQuery with a predicate that should fetch any other records that do not exist locally. The next step is basically calling the [database performQuery: inZoneWithID:completionHandler:] method.
The surprise shows up when I get the results, which contain the "Package" record E that the user previously deleted.
This doesnt seem to be right to me...
The steps I took to debug:
Right after deleting the "Package" record E, I created a CKFetchRecordsOperation and tried to fetch the deleted record. The result was as expected: I got a "Record not found". I'm cool here.
Thinking there might be some delays on the server side, I put a dispatch_after block and launched the same fetch operation I did in point 1 but just after 30 seconds. The result was still as expected: I got the "Record not found" error.
Performed the same test as I did in point 2 but with a delay of 100 seconds and ... surprise, the CKFetchRecordsOperation operation returned the deleted record E package. The weird thing is that somethings it will still return an error, but sometimes will just plainly return the deleted object.
And now the really weird part: This does not happen with record A, B, C and D, the single difference between all theses records are their names. This does not make any sense.
I filled a bug report and the reply I got was this:
This is correct behavior. Queries are eventually consistent so the deletes may not immediately be reflected when querying. Fetching the deleted record by ID via a CKFetchRecordsOperation should return a CKErrorUnknownItem right away.
While this is partially true, this does not seems to be the case with what I'm seeing.
Code
CKContainer *container = [CKContainer defaultContainer]; CKDatabase *privateDB = [container privateCloudDatabase]; CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName: @"DS2000330803AS"]; CKModifyRecordsOperation *operation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave: nil recordIDsToDelete: @[recordID]]; operation.database = privateDB; [operation setModifyRecordsCompletionBlock:^(NSArray<CKRecord *> * _Nullable savedRecords, NSArray<CKRecordID *> * _Nullable deletedRecordIDs, NSError * _Nullable error) { CKFetchRecordsOperation *fetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs:@[recordID]]; fetchOperation.database = privateDB; [fetchOperation setPerRecordCompletionBlock:^(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error){ NSLog(@"Error: %@", error.localizedDescription); }]; }];
[NSTimer scheduledTimerWithTimeInterval:100 repeats:NO block:^(NSTimer * _Nonnull timer) { CKContainer *container = [CKContainer defaultContainer]; CKDatabase *privateDB = [container privateCloudDatabase]; CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:@"DS2000330803AS"]; CKFetchRecordsOperation *fetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs: @[recordID]]; fetchOperation.database = privateDB; [fetchOperation setPerRecordCompletionBlock:^(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error){ NSLog(@"Error: %@", error.localizedDescription); }]; [privateDB addOperation: fetchOperation]; }];
CKContainer *container = [CKContainer defaultContainer]; CKDatabase *privateDB = [container privateCloudDatabase]; NSPredicate *predicate = [NSPredicate predicateWithValue: YES]; CKQuery *query = [[CKQuery alloc] initWithRecordType:@"Package" predicate:predicate]; [privateDB performQuery:query inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * _Nullable results, NSError * _Nullable error) { [results enumerateObjectsUsingBlock:^(CKRecord * _Nonnull record, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"Record ID: %@", record.recordID); CKFetchRecordsOperation *fetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs: @[record.recordID]]; fetchOperation.database = privateDB; [fetchOperation setPerRecordCompletionBlock:^(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error){ NSLog(@"Error: %@", error.localizedDescription); }]; [privateDB addOperation: fetchOperation]; }]; }];
Other notes: I removed and commented pretty much everything related to CloudKit and the above code is the only one that interacts with CloudKit. I'm testing with a single device at the moment.
I know the CKQuery can have a better NSPredicate, but now I try to understand why I have this issue.
P.s. When I added the first implementation of CloudKit to my app, I tried to keep it as simple as possible, without any fancy syncing stuff. It worked just fine for a year, then I started getting reports from my users that they cannot delete some records in production.
Any hints guys on how I should further debug this?
Thank you!
Upvotes: 3
Views: 446
Reputation: 4373
I think you are mixing up record Type and record Name(String of CKRecordID). Name is assigned by CloudKit(Typically) and type is set by you. I would bet it was auto assigned but I would have to see how the record was saved. It would be telling to see a screenshot of your CloudKit Dashboard.
In your block of code in 1) you are trying to delete the record name of some record using the record type. That is why you get the error "Record not found" 2) Same as you are still using Record Type and not record name 3) Fetches the record because it is actually using the assigned record.recordID.
This is my gut on the situation. As far as deleting and refreshing please see my answer on stitching records to keep UI and database in sync.
Upvotes: 1