Do we need a local modificationDate field?

I see the CKRecord class has a `modificationDate: Date`, but I'm tempted to add my own field for resolving conflicts, because that field represents the time the record was saved in CloudKit, not the time the change was actually made on the local device. Imagine...


- At time t=1, record A changes on device 1.

- At t=2, record A changes on device 2.

- At t=3, record A uploads to CloudKit from device 1.

- At t=4, device 2 see the change to A from device 1 and tries to resolve a conflict with "last writer wins". If it looks at the CKRecord.modificationDate, it will see the modificationDate as t=3 and it will overwrite the change from device #2 at t=2. In reality the time of the change from device #1 was t=1 and device #2's change should win.


Rob

Keep in mind that what is more important than the 'time' of any change is whether or not the version that a device thinks it is changing is the current version stored in CloudKit. That is the purpose of the logic behind:


https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation/1447488-savepolicy?language=objc

https://developer.apple.com/documentation/cloudkit/ckerror/2325208-serverrecordchanged


You can certainly use your own system, but focus on version number not absolute timing


Device 1 downloads version A of a record (e.g. you have $100 left in your account)

Device 2 downloads the version A of the record (e.g. you have $100 left in your account)

Device 1 makes a change in version A and saves it as version B (e.g. debit $20 from $100 - you have $80 left in the account)

Device 2 makes a change in version A and saves it as version C (e.g. debit $10 from $100 - you have $90 left in account) CONFLICT!! - device 2 is making a change in a record that is outdated. And error should be sent to device 2 telling it that its change may be incorrrect.

Yes, I understand this. The code I'm writing is the part that tries to resolve/merge the conflict (ideally without user interaction) after the conflict is detected, like in step 4 of your example. Depending on the data, there are different ways one might do this. For example, if record versions B and C differ by one field, which represents a "max" value, one might be able to merge them with `max(Bval, Cval)`. In some cases you can have a conflict-free replicated data type (CRDT) or something fancy (which could handle something like a bank balance), but in other cases it makes sense to just follow a "last writer wins" algorithm and choose the B or C record. If I do that, and I care about getting the times right, I need to create my own `modifiedDate` field, which seems odd.


I believe in one recent WWDC CloudKit video, Apple gave an example and mentioned "last writer wins" as a simple way to resolve a conflict, so the idea is not unheard of. I remember in that same WWDC video the presenter mentioned people creating their own `lastModified` fields in CKRecord, as if that were a mistake. But given what I described above, I see a reason to do it.

> The code I'm writing is the part that tries to resolve/merge the conflict (ideally without user interaction) after the conflict is detected


I was answering the question 'do we need a local modificationDate field' - you don't. Again - time (modifciationDate) doesn't matter, version does. CloudKit already has a serverRecordChanged error that compares version downloaded and version being changed, not times, and warns the device that there is a conflict.


That's the answer to the question asked.


Now, what to do when you detect a conflict? As you indicate, there are lots of ways of handling it. But none of those ways depends on modificationDate.

> Now, what to do when you detect a conflict? As you indicate, there are lots of ways of handling it. But none of those ways depends on modificationDate.


I'm pretty sure that last sentence is wrong. Have you heard of 'last write(r) wins'? That is a simple way of resolving the conflict, and it needs modificationDate.

'last write wins' does not refer to modificationDate it refers to the order of the write - and that is exactly what would happen if you chose CKRecordSaveAllKeys here:



https://developer.apple.com/documentation/cloudkit/ckrecordsavepolicy?language=objc

I think we’re confusing “write” operations with “sync” operations between replicas.


Sorry if this is obvious, but maybe I need to explicitly say that my app has a local cache/replica DB and can work offline. I thought that was normal when using CloudKit, so I didn’t mention it. When an app is working like this, it is effectively using an optimistically replicated database, where each device is a replica. CloudKit is like a replica too, but a special one.


Let’s say a user has 2 devices and both are offline. Then…


1. They save something with iPhone on Tuesday.

2. They save it again with iPad on Wednesday.

3. iPad reconnects and writes (syncs) the change to CloudKit.

4. iPhone reconnects…


The bottom line is that I think it makes sense here to have #2 (the iPad change) win. But if the iPhone uploads its change in step #4 with a savePolicy of `.allKeys` like you mentioned, won’t it simply overwrite the record from the iPad?


I think that #2 is the "last write" from the user’s perspective. I also think it is what "last write" typically means in the context of replicated databases. The other write ops, to CloudKit, in steps #3 and #4, are not really writes to the overall database - they are the replicas syncing up their data - and their timing should be irrelevant to which record wins.

> Sorry if this is obvious, but maybe I need to explicitly say that my app has a local cache/replica DB and can work offline. I thought that was normal when using CloudKit, so I didn’t mention it.


You obviously have a vision for how you want this to work. I thought you were unaware of the system built into CloudKit that handles these conflicts so I mentioned it. But you can certainly ignore the system that is integral to CloudKit and make your own. Best of luck to you!

Do we need a local modificationDate field?
 
 
Q