Skip to content

GO-7357 Fix state-apply correctness bugs in editor object sync - #3195

Open
requilence wants to merge 4 commits into
developfrom
tmp/state-apply-bugfixes
Open

GO-7357 Fix state-apply correctness bugs in editor object sync#3195
requilence wants to merge 4 commits into
developfrom
tmp/state-apply-bugfixes

Conversation

@requilence

Copy link
Copy Markdown
Contributor

Summary

Four confirmed-by-test correctness fixes in the smartblock state-build / change-apply pipeline, found during an offline-collaboration review of editor objects. Each has a test that fails before the fix and passes after. Linear: GO-7357.

Fixes

  1. Prevent block loss on conflicting concurrent moves (state/change.go)
    Cross-moves (device A: x→under y, device B: y→under x) created a cycle detached from the root; apply() then garbage-collected both subtrees on every device. Move-into-a-concurrently-deleted-target lost the moved subtree the same way. Now a move whose target sits inside the moved blocks is rejected (the earlier concurrent move wins deterministically), and blocks whose target was removed are reattached to the root instead of being GC'd.

  2. Keep copy-on-write in normalizeRecommendedRelations (state/normalize.go)
    When the derived state had no own details, normalization mutated the committed parent state's details in place, bypassing copy-on-write. Now reads via Details() and writes via SetDetail only when values actually change.

  3. Reset changesSinceSnapshot when an appended batch contains a snapshot (sourceimpl/source.go)
    treeSource.Update captured prevSnapshot after overwriting lastSnapshotId, so the reset branch was dead code and the counter only accumulated — inflating snapshot probability and, with it, the rate of conflicting concurrent snapshots. BuildState now reports whether a snapshot change was applied, and Update restarts the counter from it.

  4. Keep snapshot data when unmarshalling snapshot changes (sourceimpl/source.go)
    NewUnmarshalTreeChange kept the snapshot only for the first converted change. objecttree caches the result as Change.Model and discards the raw data, so a snapshot change converted second lost its snapshot; a later in-memory tree reduce that made it the root left BuildState with a nil snapshot and the object stayed stale until reopened. Now the snapshot is kept for changes flagged IsSnapshot. Also raised Update/Rebuild listener failures from debug to error level so frozen objects are visible.

Testing

  • go test ./core/block/editor/state/ ./core/block/source/sourceimpl/ — green
  • New tests: TestState_ApplyChange_MoveConflicts, TestNormalizeRecommendedRelationsDoesNotMutateParent, TestTreeSource_Update_ChangesSinceSnapshot, TestNewUnmarshalTreeChange_KeepsSnapshotForSnapshotChanges

Notes

These fixes change replay semantics for conflict cases; mixed old/new client versions will disagree on the outcome until the next snapshot bakes one side in — inherent to any replay fix. Broader design-level findings from the review (receive-path normalization divergence, version flip-flop via snapshots, whole-value LWW on list relations) are intentionally out of scope here.

A move whose target was concurrently moved inside one of the moved blocks
created a cycle detached from the root, and apply() garbage-collected both
subtrees on every device. Reject such moves so the previously applied
concurrent move wins deterministically.

A move whose target was concurrently removed left the moved blocks unlinked,
so they were garbage-collected as unreachable. Reattach them to the end of
the root instead of losing them.
When the derived state had no own details, normalization mutated the parent
(committed) state's details in place, bypassing copy-on-write. Read through
Details() and write through SetDetail instead, so the committed state stays
untouched and the normalized values participate in the regular diff.
…snapshot

treeSource.Update captured prevSnapshot after overwriting lastSnapshotId, so
the reset branch was unreachable and the counter only accumulated, inflating
snapshot probability and with it the rate of conflicting concurrent snapshots.

The root-change proxy could not work anyway: in append mode the root does not
change in the same update that delivers a snapshot change. BuildState now
reports whether a snapshot change was applied, and Update restarts the counter
from it.
NewUnmarshalTreeChange kept the snapshot only for the first converted change.
A snapshot change arriving in an append batch after another new change was
unmarshalled without its snapshot; objecttree caches the result as Change.Model
and discards the raw data, so when an in-memory tree reduce later made that
change the root, BuildState failed with a nil snapshot and the object stayed
stale until reopened. Keep the snapshot for changes flagged IsSnapshot.

Also log Update/Rebuild listener failures at error level: they leave the live
state behind the tree heads, and at debug level frozen objects were invisible.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant