I want to pull down around 500 "Visit" records from the public database. CloudKit only gives you 100 records at a time so I just utilize the CKQueryCursor like below to get all the records I want.
func fetchVisits(_ cursor: CKQueryCursor? = nil) {
print("fetchVisits \(cursor)")
var operation: CKQueryOperation!
if let cursor = cursor {
operation = CKQueryOperation(cursor: cursor)
} else {
let query = CKQuery(recordType: "Visit", predicate: NSPredicate(format: "Client IN %@ AND ANY Students IN %@", visitClients, visitStudents))
operation = CKQueryOperation(query: query)
}
operation.recordFetchedBlock = {
(record) in
totalVisits.append(record)
}
operation.queryCompletionBlock = {
(cursor, error) in
if let error = error {
//handle error
} else if let cursor = cursor {
self.fetchVisits(cursor)
} else {
//all done!
}
}
CKContainer.default().publicCloudDatabase.add(operation)
}
I call the function like:
fetchVisits()
That works fine, it gets all the visits I need. Console Log
fetchVisits nil
fetchVisits Optional(<CKQueryCursor: 0x174228140; id=4bb7887c326fc719, zone=(null)>)
fetchVisits Optional(<CKQueryCursor: 0x17422a320; id=f67fb25669486da9, zone=(null)>)
fetchVisits Optional(<CKQueryCursor: 0x174228380; id=7e87eb8b7cfe1a74, zone=(null)>)
fetchVisits Optional(<CKQueryCursor: 0x17422cc80; id=e77e47ef2b29c8a4, zone=(null)>)
But the issue is now I want to refresh when I push a button and now it gives me this error:
"Service Unavailable" (6/2022); "Request failed with http status code 503"; Retry after 30.0 seconds
Which is pretty self explanatory, I guess I am overwhelming the server by requesting a measly 500 records? So I wait 30 seconds and invoke the function again and now I get this error.
"Limit Exceeded" (27/2023); server message = "Query filter exceeds the limit of values: 250 for container
For some reason I cannot run that function again. If I restart the app it works fine again but only the first time. This issue seems to be specific to any table that returns a CKQueryCursor. I have other tables that I'm hitting that have less than 100 records (so the cursor is nil) and I can pull those multiple times without any issues.
Ok, so I've seen this before and the issue is, I believe, a bug in on the CloudKit servers. In my experience, it has to do with complex queries.
If you try changing your predicate to:
NSPredicate(value: true)
Or even simplifying your existing one by removing the ANY part, that may be enough to fix it.
You're requesting more CKRecords to iCloud before your query operation finished.
...
operation.queryCompletionBlock = {
(cursor, error) in
if let error = error {
//handle error
} else if let cursor = cursor {
self.fetchVisits(cursor)
} else {
//all done!
}
}
...
Function self.fetchVisits(cursor) call is done inside the completion block, this means that your are requesting for more records before your current operation has finished.
Possible solution is to use a closure (completionHandler) in which you include the CKQueryCursor, when user need more records (scroll a table or whatever) you call one again self.fetchVisits passing the cursos received on your closure.
EDIT: I think by now I found the reason:
While my previous answer (below) is right, it does not apply to this problem.
By now, I encountered myself the two server errors mentioned in the question, and investigated the situation.
My setup is the following:
I want to download a subset of iCloud records depending on a condition specified in a predicate:
let doNotDownloadIfOlderFormat = "NOT " + iCloudRecordKeyName + " IN %@"
let doNotDownloadIfOlderPredicate = NSPredicate.init(format: doNotDownloadIfOlderFormat, doNotDownloadIfOlder)
Here, doNotDownloadIfOlder is a set of custom objects.
In some test cases, their number was more than 300. I suspected that this could be the reason for the error message Query filter exceeds the limit of values: 250 for container.
I thus modified my code:
let doNotDownloadIfOlderFormat = "NOT " + iCloudRecordKeyName + " IN %@"
let limit = min(doNotDownloadIfOlder.count, max)
let shortened = Set(Array(doNotDownloadIfOlder)[0 ..< limit])
let doNotDownloadIfOlderPredicate = NSPredicate.init(format: doNotDownloadIfOlderFormat, shortened)
where I set max to a test value below 300.
This is the result:
Obviously there is a probably undocumented server limit for queries. The strange number 140 could have something to do with the size of my custom objects.
Previous answer:
The docs say:
The server can change its limits at any time, but the following are general guidelines:
- 400 items (records or shares) per operation
- 2 MB per request (not counting asset sizes)
If your app receives
CKError.Code.limitExceeded, it must split the operation in half and try both requests again.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With