Getting CKErrorServerRecordChanged error with no server record in userInfo. Instead there's an underlying error "SaveSemantics is failIfExists, existing record has chaining, new record does not"

When I run my app and do a CKModifyRecordsOperation to save a record that already exists on the server (with no changetag) to test merging I do indeed get a CKErrorServerRecordChanged.


There is no values in the user info dictionary of this error for CKRecordChangedErrorClientRecordKey or CKRecordChangedErrorServerRecordKey. Instead there is an NSUnderlying error which prints out:


server message = "SaveSemantics is failIfExists, existing record has chaining, new record does not"


So, I was able to track down the cause to hit this condition. The server record is a parent to an existing record. I'm not sure why being the parent would prevent the normal merging flow. In order to "merge" with the server record, I have to create a query operation and pull it down, instead of just getting the record from the user info.

I was just using parents for potentially future use with sharing (currently not used in my app). But I think i'll just remove parent references because they seem to cause too many problems.


FB7440624

Replies

Could the problem be that the form of the record you are trying to save (i.e. "just getting the record from the user info") does not have a link to that child record but the record that is on the server does have that link. Therefore there is an inherent conflict - should the new saved record (the one you are trying to save) not have that link or should it retain that link? The system has no idea. But it does recognize that its current version of the record has information in it that the new version you are trying to save never knew about. As you mentioned, one solution is to start with a query of the current record, modify that and save it. That way you can only conflict with changes that were since the query response, a fraction of a second before the save. In your case, by using "the record from the user info" you may be openning yourself to perceived conflicts over the life of the record.

Yes there is a conflict. The record on the server is the parent of another record. That relationship gets set on the child record (via the parent property).


So say, I have a parent record Drinks. Under drinks child records are Milk and Juice.


Now device number 2 comes along and also creates a record named Drinks but does not have the one on the server yet (obviously). Also device 2 does not create Milk and Juice (or any children for that matter). So device 2 sends its version of Drinks to the server, and I get the server record changed error. How would I handle it? In this case, I'd cache the server records metadata of drinks in my local cache. Later (hopefully soon) I'll get a notification and my fetch zone changes operation will pull down Milk & Juice.


In this case, I'm not sure what the benefit is to not providing the server record in the user info in order to handle the error? Why require me to fetch the server record instead of putting it in the userInfo as always? The way to merge a client record is to take the properties and apply them to the server record, and then re-upload them to Cloudkit. Other objects that have this record as their parent shouldn't be bothered.


Ended up removing parent references because I'm not sharing right now and it doesn't seem worth it, even though my 'workaround' of fetching the record using a query works.


I do still point this record to another record using a CKReference with delete self as the action (so when Drinks gets deleted, Milk and Juice also get deleted automatically). When testing under those conditions, I have been able to get a CKErrorServerRecordChanged error, but I also get the server record in the user info. I'm staying away from the parent property I guess.

TMI.


What you describe is a conflict before you get to 'parent/child relationships'. Simply...."....device number 2 comes along and also creates a record named Drinks..." - just this alone is a problem. The current record "Drinks" already exists and could have lots of information in it (not parent/child) that describes years and years of history with milk, juice and maybe even soda. But this 'device number 2' record is unaware of that history. What do you want the server to do - lose all that information or ignore the new record? You need to provide more guidance to handle such conflicts. But, better, you need to avoid conflicts in the first place by first querying the database for an existing, relevant record and then either create a new one (if none exists) or amend the existing one based upon whether you get to start with a clean slate of drinks or whether you can never have those sugary kinds.

>What you describe is a conflict before you get to 'parent/child relationships'. Simply...."....device number 2 comes along and also creates a record named Drinks..." - just this alone is a problem. The current record "Drinks" already exists and could have lots of information in it (not parent/child) that describes years and years of history with milk, juice and maybe even soda. But this 'device number 2' record is unaware of that history. What do you want the server to do - lose all that information or ignore the new record?


Not at all. I want to handle the error (this discussion is about error handling) in a way that makes sense. There really should be no difference in how I end up merging this record. I either get the server record from the user info and sniff it, and do whatever merging makes sense. But since it's not there (in this edge case) I go and shoot a query to get the server record myself, and do the exact same thing.


For whatever reason, under this condition (with the 'conflicted' record being a parent of some other record), Cloudkit doesn't hand me the server record and I have to fetch it. *If* there was years and years of history, it wouldn't be lost in either case as part of this merge. I'm merging on the server record (I'm not deleting anything). In my simple case here anyway, I'm just archiving the server metadata.

I guess my point is that, to avoid conflicts, you first try to download an existing record, make modifications to the existing record and then save the modified record. That's why it's called 'CKModifyRecordsOperation'. If you do that you don't need to 'handle the error'. It's not an edge case. The edge case occurs when the record doesn't exist.

>I guess my point is that, to avoid conflicts, you first try to download an existing record, make modifications to the existing record and then save the modified record. That's why it's called 'CKModifyRecordsOperation'. If you do that you don't need to 'handle the error'.


Yeah, I prefer not to do that though for the most part for this app; I don't see much of a benefit to that approach in my situation. No reason to add an extra network request every time I want to work on a record. My app has lots of small records that can be edited, and operations that can edit lots of data in bulk. No reason to pull all the CKRecords down from the server to make changes every time. Makes a lot more sense to send the changes to the server and handle errors with a merge policy for this app, rather than look before you leap.


Also I'd like my app to work when there is no network connection and I don't want to hit the network an extra time (potentially) everytime I'm trying to change something. So I modify my local cache, send the change to the server. If there's an error merge when possible without user interaction. If there's a network error the operation will be retried automatically. If there's some sort of failure, show an alert and rollback the local cache.


> It's not an edge case. The edge case occurs when the record doesn't exist.


There is no mention in the documentation of the possibility of getting a CKErrorServerRecordChanged without the expected values in the userInfo (at least not in any of the documentation I was able to find). Instead CKErrorServerRecordChanged documentation states:


This error indicates that the server's version of the record is newer than the version the client tried to save. Your app is expected to handle this error, resolve any conflicts in the record, and attempt another save of the record, if needed.

CloudKit provides your app with three copies of the record in this error's

userInfo
dictionary to assist with comparing and merging the changes:

When a conflict occurs, your app should merge all changes onto the record under the CKRecordChangedErrorServerRecordKey key and attempt a new save using that record. Merging onto either of the other two copies of the record results in another conflict error because those records have the old record change tag.


So I called it an edge case, because getting a CKErrorServerRecordChanged error is expected. But getting a CKErrorServerRecordChanged with no useful userInfo is not.

But won't this always happen to you:

"This error indicates that the server's version of the record is newer than the version the client tried to save."

Not always. I handle the error by merging on the server record, and caching the server record metadata locally (which should have the latest changetag). If the client's changetag matches the server's changetag, that client won't get the error when it writes the next change to the server (unless another client writes a change first, then I'd have to merge again).

> Now device number 2 comes along and also creates a record named Drinks but does not have the one on the server yet (obviously). Also device 2 does not create Milk and Juice (or any children for that matter). So device 2 sends its version of Drinks to the server, and I get the server record changed error. How would I handle it? In this case, I'd cache the server records metadata of drinks in my local cache. Later (hopefully soon) I'll get a notification and my fetch zone changes operation will pull down Milk & Juice.


My 2 cents is that this error calls for an active & major resolution to ensure the devices are back in sync before waiting on the next fetch to travel down with specific records that need updating. If something happens and you can't get those fetches (CloudKit or device move offline, record changes lost, device termination, etc), it can make resolving your records with a users new changes to the local store a nightmare as they queue on either side. It also can/tends to affect large sets of records.


I now assume that the entire local store of relevant objects on device number 2 is entirely out of sync with the cloud records, so instead of trying to resolve it on a scoped single record or record chain basis (Drink, Milk & Juice), I pull down every record in both parent, child record types in the zone and compare, merge them into my current device model with varying degrees of specificity. This helps to avoid records ghosting out on Device 2, while existing on Device 1 and in the cloud. The assumption here being the worst: you'll never get those record changes (changesets have been lost in some way, things have gotten too complex to resolve, or the user is making major conflicts before fetches complete [which sum up the situations when I'd most often experience this error]).


From there I split untouched model objects away from those that are new, newer on device, or deleted (aided by a tombstone) and push those changes back into the cloud store. The goal being to resolve all changes in this store in one change cycle. Device 1, 3, 4 receive the updated changes from the Cloud notification or launch fetch changes operation to bring them both to partiy with each other. If one of those devices enters a similar state in the meantime as 2, they can do the same flush, repeating the cycle.


Edit, I completely misintpreted your original question: That's (seemingly) bizarre behavior that I'm not getting and can't find information on the net about. When I push a locally created parent record from an object, nil recordChangeTag (which also lacks the CK metadata that exists on its cloud record version and has children records in the cloud) it successfully merges, no fuss. This is one of the first things I tested after I enabled sharing by making sure document titles synced!


I definitly do not get a kickback requiring me to pull down the cloud metadata at all.

Thanks for the replies guys.



>Edit, I completely misintpreted your original question: That's (seemingly) bizarre behavior that I'm not getting and can't find information on the net about. When I push a locally created parent record from an object, nil recordChangeTag (which also lacks the CK metadata that exists on its cloud record version and has children records in the cloud) it successfully merges, no fuss. This is one of the first things I tested after I enabled sharing by making sure document titles synced!


That's weird. My locally created parent is something like this:


CKRecordZoneID *zoneID = [[CKRecordZoneID alloc]initWithZoneName:@"LilPrivZone" ownerName:CKCurrentUserDefaultName];
  CKRecordID *recordID = [[CKRecordID alloc]initWithRecordName:@"Drinks" zoneID:zoneID];
 CKRecord *record = [[CKRecord alloc]initWithRecordType:@"Group" recordID:recordID];
 [record setObject:@"Drinks" forKey:@"ui_title"];


No change tag, no references locally. Send that to the server by itself and if Drinks exists (with the same record name of course) and it is a parent of another record on the server I get the conflict error with no user info.

Yep, that's essentially the same thing that I am doing and I get no kickback unless I attempt to delete a parent with children, and even in that case I'm provided with a different error under the CKErrorReferenceViolation branch. I even just tested it and am not having issues what-so-ever. Currently using a custom zone in the private container, made using the save method on the privateDB (I assume all capabilities enabled). Using CKModifyRecordsOperation to push my changes to the record, just as you are.


Oh, but I did just dig up that I change the default savePolicy property on the Operation! The default CKModifyRecordsOperation.RecordSavePolicy is .ifServerRecordUnchanged but for this type of operation it should probably be set to .changedKeys. Then keys you're not interacting with won't be touched.

Yea, my operation uses CKRecordSaveIfServerRecordUnchanged policy. The definition of CKRecordSaveIfServerRecordUnchanged seems to describe my intent more accurately than CKRecordSaveChangedKeys. Changing the save policy may work in this situation (haven't tried, I just got rid of the parent reference since I was kind of reserving it for potential future use anyway) since the record is pretty basic.


I'd rather not use CKRecordSaveChangedKeys policy for the most part because it seems like a pretty easy way to make a mistake and overwrite data on the server I'm not trying to overwrite. I feel like I'm less likely to make a mistake using CKRecordSaveIfServerRecordUnchanged. If I'm doing it as part of error handling, I really want to do it. Just kind of is how my mind works I guess.

Ah I totally see what you're saying! Very frustrating.


I wish we could get more insight to why this error is being put up. And why CloudKit error is lacking the data to actually resolve it because I'm surely going to hit this issue in the future.


I'm really confused because I'm not sure what is actually related to the chain on the parent record, besides its existance. Did you figure that out?