-
Notifications
You must be signed in to change notification settings - Fork 74
Handle 'batchRequestFailed' errors #328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for addressing the issue from #315!
I’ve run manual tests both in the CloudKitDemo sample app and in my own project, where I originally discovered the issue, and everything worked fine!
Logs from CloudKitDemo
Device B: Update c39453bb & insert 6ee059ba
SQLiteData (private.db) nextRecordZoneChangeBatch: scheduled
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ event ┃ recordType ┃ recordName ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ➡️ Sending │ counters │ 6ee059ba-3681-4625-b859-dff3ed59cfbd:counters │
│ ➡️ Sending │ counters │ c39453bb-076d-4498-a30b-1a58f995db2f:counters │
└───────────┴────────────┴───────────────────────────────────────────────┘
Timestamp: 2025-12-13 08:53:32.321688+07:00
Device A: Update c39453bb & insert fd6a5d63
SQLiteData (private.db) nextRecordZoneChangeBatch: scheduled
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ event ┃ recordType ┃ recordName ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ➡️ Sending │ counters │ fd6a5d63-f031-48fc-b661-b7aa1d7ceb87:counters │
│ ➡️ Sending │ counters │ c39453bb-076d-4498-a30b-1a58f995db2f:counters │
└───────────┴────────────┴───────────────────────────────────────────────┘
Timestamp: 2025-12-13 08:53:32.608295+07:00
Device B: Save changes to c39453bb & 6ee059ba
SQLiteData (private.db) sentRecordZoneChanges
┏━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ action ┃ recordType ┃ recordName ┃
┡━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ✅ Saved │ counters │ 6ee059ba-3681-4625-b859-dff3ed59cfbd:counters │
│ ✅ Saved │ counters │ c39453bb-076d-4498-a30b-1a58f995db2f:counters │
└─────────┴────────────┴───────────────────────────────────────────────┘
Timestamp: 2025-12-13 08:53:33.372963+07:00
Device A: Fail saving changes to c39453bb & fd6a5d63
SQLiteData (private.db) sentRecordZoneChanges
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ action ┃ recordType ┃ recordName ┃ error ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 🛑 Save failed │ counters │ c39453bb-076d-4498-a30b-1a58f995db2f:counters │ serverRecordChanged (14) │
│ 🛑 Save failed │ counters │ fd6a5d63-f031-48fc-b661-b7aa1d7ceb87:counters │ batchRequestFailed (22) │
└───────────────┴────────────┴───────────────────────────────────────────────┴──────────────────────────┘
Timestamp: 2025-12-13 08:53:34.554332+07:00
Device A: Fetch changes to c39453bb & 6ee059ba from server
SQLiteData (private.db) fetchedDatabaseChanges
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
┃ action ┃ zoneName ┃ ownerName ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
│ ✅ Modified │ co.pointfree.SQLiteData.defaultZone │ __defaultOwner__ │
└────────────┴─────────────────────────────────────┴──────────────────┘
Timestamp: 2025-12-13 08:53:37.376031+07:00
SQLiteData (private.db) fetchedRecordZoneChanges
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ action ┃ recordType ┃ recordName ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ✅ Modified │ counters │ c39453bb-076d-4498-a30b-1a58f995db2f:counters │
│ ✅ Modified │ counters │ 6ee059ba-3681-4625-b859-dff3ed59cfbd:counters │
└────────────┴────────────┴───────────────────────────────────────────────┘
Timestamp: 2025-12-13 08:53:37.756558+07:00
Device A: Send changes to c39453bb & fd6a5d63
SQLiteData (private.db) nextRecordZoneChangeBatch: scheduled
┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ event ┃ recordType ┃ recordName ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ➡️ Sending │ counters │ fd6a5d63-f031-48fc-b661-b7aa1d7ceb87:counters │
│ ➡️ Sending │ counters │ c39453bb-076d-4498-a30b-1a58f995db2f:counters │
└───────────┴────────────┴───────────────────────────────────────────────┘
Timestamp: 2025-12-13 08:53:37.776724+07:00
SQLiteData (private.db) sentRecordZoneChanges
┏━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ action ┃ recordType ┃ recordName ┃
┡━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ✅ Saved │ counters │ fd6a5d63-f031-48fc-b661-b7aa1d7ceb87:counters │
│ ✅ Saved │ counters │ c39453bb-076d-4498-a30b-1a58f995db2f:counters │
└─────────┴────────────┴───────────────────────────────────────────────┘
Timestamp: 2025-12-13 08:53:38.434067+07:00
Device B: Fetch changes to all touched records
SQLiteData (private.db) fetchedDatabaseChanges
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
┃ action ┃ zoneName ┃ ownerName ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
│ ✅ Modified │ co.pointfree.SQLiteData.defaultZone │ __defaultOwner__ │
└────────────┴─────────────────────────────────────┴──────────────────┘
Timestamp: 2025-12-13 08:53:39.692850+07:00
SQLiteData (private.db) fetchedRecordZoneChanges
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ action ┃ recordType ┃ recordName ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ✅ Modified │ counters │ 6ee059ba-3681-4625-b859-dff3ed59cfbd:counters │
│ ✅ Modified │ counters │ fd6a5d63-f031-48fc-b661-b7aa1d7ceb87:counters │
│ ✅ Modified │ counters │ c39453bb-076d-4498-a30b-1a58f995db2f:counters │
└────────────┴────────────┴───────────────────────────────────────────────┘
Timestamp: 2025-12-13 08:53:40.139455+07:00
SyncEngine is now aware of the atomicity flag. Since we never want to explicitly configure it to behave atomically, as CKSyncEngine does not guarantee how changes are batched together anyway, I wonder whether it would be more fitting to move the flag into SyncEngine.processPendingRecordZoneChanges(…) and use it to simulate CloudKit’s incorrect behavior in a more contained way, rather than having full support for the flag. That said, it’s more of a detail.
| } | ||
|
|
||
| @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) | ||
| @Test func deleteConflictAndNewRecord() async throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deleteConflictAndNewRecord() test is failing for me. It’s missing inline snapshots for the assertions, and even after generating them, the second call to processPendingRecordZoneChanges(scope:) has no pending record zone changes to process. In other words, the batch for the composite modification succeeds, with record 1 deleted and record 2 inserted.
I initially planned to propose adding change tag comparison for deletes to the mock implementation of modifyRecords(…), similar to what’s done for saves. However, after testing the live behavior, it appears there’s no special handling for this case.
From my observation, a conflicting update and deletion behaves as follows:
- Update first: if the update reaches the server first, a subsequent deletion is accepted no matter the change tag, resulting in deletion of the record.
- Deletion first: if the deletion reaches the server first, a subsequent update results in an
.unknownItempartial error. SQLiteData handles this by effectively re-saving the record from scratch and resurrecting the record.
This test falls into the first scenario, which is not expected to produce a partial error, so the test fails. If we want to test deletions as part of an atomicity test, I see two options:
- Keep the structure of this test, but swap the update and deletion of record 1. In the composite modification, this would hit the second scenario and produce the
.unknownItemerror. - Move the conflict to a concurrent update of a record, include a deletion in the composite modification, and have it fail with a
.batchRequestFailedpartial error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2. Move the conflict to a concurrent update of a record, include a deletion in the composite modification, and have it fail with a
.batchRequestFailedpartial error.
Yeah, this is the test I meant to write but did not finish it. I have just pushed an update.
|
Hi @lukaskubanek, taking the time to look through the PR.
The atomic flag was just forced the sync engine into this state that we feel is actually buggy on Apple's side. It's not publicly exposed and only tweaked in tests. But, I do think pushing it to |
lukaskubanek
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
I ran another manual test and everything seems to work as before.
| return (saveResults: saveResults, deleteResults: deleteResults) | ||
| } | ||
|
|
||
| return (saveResults: saveResults, deleteResults: deleteResults) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can’t we directly return return storage.withValue { … } rather than storing the result of this expression in additional variables and returning those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, we can do that now. Think previously we had work done after the withValue, but that's no longer the case. Thanks!
Fixes #315
There appears to be a bug in iCloud/CloudKit that causes
.batchRequestFailederrors to be emitted even when operations are not atomic. We aren't sure of the exact conditions that causes the error to be emitted, but regardless let's start handling that error. We will simply re-enqueue those records to be saved/deleted, and then they should succeed just fine.