Search code examples
swiftmultithreadingcloudkitnserrorckrecord

CloudKit error handling - retry logic


I want to put excellent CloudKit error handling in my app as Apple wants us to do. I want to save and modify a record right now. Here is my basic save logic...

func addNewRecord(managedObj: NSManagedObject) {
  let newRec = managedObj.convertToCkRecord()
  publicDB.saveRecord(newRec, completionHandler: saveHandler)
}

func saveHandler(savedRecord: CKRecord?, error: NSError?) {
  // handle errors here
  if let error = error {

    if error.code == CKErrorCode.NotAuthenticated.rawValue {
      // debug
      print("Not authentricated")
    }
    else if error.code == CKErrorCode.NetworkFailure.rawValue {
      print("Network failure!!")
      if let retryAfterValue = error.userInfo[CKErrorRetryAfterKey] as? NSTimeInterval {

        let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(retryAfterValue * Double(NSEC_PER_SEC)))
        dispatch_after(delayTime, dispatch_get_main_queue()) {
          // THIS IS WHERE I GET STUCK, WHERE DO I FIND THE FAILED CKRECORD FOR RETRY?  
          // IS IT IN USERINFO SOMEWHERE?
          //self.saveHandler(savedRecord, error: error)
        }
      }
    }
  }
  else {
    print("Save was a success! \(savedRecord)")
  }

}

This seems it should be basic, but every example I see about this just has a comment //retry or //handle error, including in the WWDC tutorials where the error handling should go. What I want to know is how do I find a reference to my failed CKRecord? Storing it in a local variable seems like it won't work because of the multithreading issue. I tried adding it to a queue, but other threads could get to that queue as well so I worry about race conditions.


Solution

  • Instead of using an NSTimer, use dispatch_after.

    print("Network failure!!")
    if let retryAfterValue = error.userInfo[CKErrorRetryAfterKey] as? NSTimeInterval {
        let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(retryAfterValue * Double(NSEC_PER_SEC)))
        dispatch_after(delayTime, dispatch_get_main_queue()) {
            saveHandler(saveRecord: saveRecord, error: error)
        }
    }
    

    Here is a helper method (in Objective-C) that I use for all record modification and deletion. It deals with common errors and retries.

    - (void)modifyRecords:(NSArray<CKRecord *> *)records andDeleteRecords:(NSArray<CKRecordID *> *)deleteIds completion:(void (^)(NSArray<CKRecord *> *savedRecords, NSArray<CKRecordID *> *deletedRecordIDs, NSError *error))completion {
        CKModifyRecordsOperation *op = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:records recordIDsToDelete:deleteIds];
        op.savePolicy = CKRecordSaveAllKeys;
        op.modifyRecordsCompletionBlock = ^(NSArray *savedRecords, NSArray *deletedRecordIDs, NSError *operationError) {
            NSError *returnError = operationError;
            if (operationError) {
                switch (operationError.code) {
                    case CKErrorRequestRateLimited:
                    case CKErrorServiceUnavailable:
                    case CKErrorZoneBusy:
                    {
                        double delay = 3.0;
                        NSNumber *delayVal = operationError.userInfo[CKErrorRetryAfterKey];
                        if (delayVal) {
                            delay = delayVal.doubleValue;
                        }
    
                        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
                            [self modifyRecords:records andDeleteRecords:deleteIds completion:completion];
                        });
                    }
                        return;
                    case CKErrorPartialFailure:
                    {
                        if (savedRecords.count || deletedRecordIDs.count) {
                            returnError = nil;
                        }
                        break;
                    }
                    default:
                    {
                        NSLog(@"Unhandled error in modify/deleteRecords: %@", operationError);
                    }
                        break;
                }
            }
    
            if (completion) {
                completion(savedRecords, deletedRecordIDs, returnError);
            }
        };
    
        [someCloudKitDatabase addOperation:op];
    }
    

    If you only want to add/modify a record, pass nil to the deleteIds parameter.

    Here's the same helper method in Swift 3 (this has not been tested but it does compile except for the last line).

    func modifyRecords(_ records: [CKRecord]?, andDelete deleteIds: [CKRecordID]?, completionHandler: @escaping ([CKRecord]?, [CKRecordID]?, Error?) -> Void) {
        let op = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: deleteIds)
        op.savePolicy = .allKeys
        op.modifyRecordsCompletionBlock = { (_ savedRecords: [CKRecord]?, _ deletedRecordIds: [CKRecordID]?, _ operationError: Error?) -> Void in
            var returnError = operationError
            if let ckerror = operationError as? CKError {
                switch ckerror {
                case CKError.requestRateLimited, CKError.serviceUnavailable, CKError.zoneBusy:
                    let retry = ckerror.retryAfterSeconds ?? 3.0
                    DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retry, execute: {
                        modifyRecords(records, andDelete: deleteIds, completionHandler: completionHandler)
                    })
    
                    return
                case CKError.partialFailure:
                    if (savedRecords != nil && savedRecords!.count > 0) || (deletedRecordIds != nil && deletedRecordIds!.count > 0) {
                        returnError = nil
                    }
                default:
                    break
                }
            }
    
            completionHandler(savedRecords, deletedRecordIds, returnError)
        }
    
        someCloudKitDatabase.add(op)
    }