CKError: Query filter exceeds the limit of values: 250 for container

769 views Asked by At

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.

3

There are 3 answers

1
Travis M. On BEST ANSWER

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.

3
Adolfo On

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.

0
Reinhard Männer On

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:

  • max <= 140: No error
  • 141 <= max <= 250: "Service Unavailable" (6/2022); "Request failed with http status code 503"; Retry after 30.0 seconds>
  • max > 250: "Limit Exceeded" (27/2023); server message = "Query filter exceeds the limit of values: 250 for container

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.