fix(connections): stop a sync acknowledging records a store refused to write - #2623
Merged
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reported as a known residual on #2619 and fixed here. Written on top of #2622 and rebased onto main
once that merged, so this is a plain branch off main.
What was wrong
applyPullResultsaved the server change token before applying anything, so a record that couldnot be written was acknowledged anyway and CloudKit never sent it again. Recovering needed a full
resync, which nothing asks for on its own.
Underneath it, the reason the coordinator could not have known better: a
Boolcannot answer thisquestion.
applyRemoteConnectionreturnedfalsefor a tombstoned record, for a record thatchanged nothing, and for a store that refused the write, and the caller read all three as "no
change". #2619 and #2622 then widened the conflation by adding an equality guard to the group and
tag stores, so
falsepicked up a fourth meaning.The record cache had the same problem one line further down. It stored every record in the batch
whether or not the batch applied, and
mergeLocalEditsreads that cache as the base a later editmerges against, so a record that was never written could still stand in as the base for one that
was.
What changed
RemoteApplyOutcomewith three cases:applied,skipped,failed. The connection, group andtag arms return it, and
GroupStorage.applyRemoteGroupandTagStorage.applyRemoteTagreturn ittoo, so "identical, nothing to write" stops sharing an answer with "the store refused".
applyRemoteChangesreports whether every record that can say so was persisted.and left unacknowledged, so the next one replays the batch.
Replaying is safe by construction: each apply writes by id, and the equality guards mean a record
that already landed writes nothing the second time.
What this does not cover
The settings, SSH profile, table favorite, database favorite and SQL favorite arms return nothing
to say whether they persisted, so they cannot participate and a failure in one of them still
acknowledges the batch. Giving each of them a result is a change per store rather than a change to
the pull, and none of them has the refuse-to-overwrite guard that made this reachable for
connections, groups and tags.
Verification
verify.sh generate,verify.sh build: PASSverify.sh test GroupStorageTests TagStorageTests SyncCoordinatorTokenExpiryTests StringCatalogIntegrityTests: PASSswiftlint --stricton every changed file: cleanThe three outcomes are tested on both stores: an unchanged record reports
skippedand leavesnothing dirty, a new one reports
applied, and one applied over an unreadable store reportsfailed. That is where the conflation lived and where a regression would land.The coordinator's own aggregation is not unit-tested.
applyPullResultis private on a type whoseonly test suite exercises a static classifier, and constructing a
SyncCoordinatorneeds CloudKitplumbing that does not exist in the test target; building it to cover
!outcomes.contains(.failed)would be a harness larger than the logic it checks.
https://claude.ai/code/session_01L81TaoPWxkLd15CGw2riPq