RTC: fix interaction with autosave that can cause posts and revisions to get lost even when a single user is editing a post#77865
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
dmsnell
left a comment
There was a problem hiding this comment.
I’m giving this an anticipatory approval because the backport branch was passing with the updates before your last commits.
I will update the backport branch with those updates and make sure that the Core test suite passes, then we can merge.
This is a companion change to WordPress/gutenberg#77865.
|
All green in WordPress/wordpress-develop#11691 |
…sions to avoid post loss. Trac ticket: Core-65138 Follow-up to #75105 Part of #77716 See Core backport in WordPress/wordpress-develop#11691 When an RTC session in the editor saves edits on a draft post, they always save into an autosave revision. This prevents a sync but where storing to the post itself would lead to duplicate edits. This was set in 9df142b839320316b406ee1a02e23704d42f8719. However, there is an exception to this rule: the first session to save edits needs to promote the autosave revision into a real draft post, if a post doesn’t exist. The reason is that WordPress hides autosaves. That is, multiple people could be editing a draft post and then once they close it, never be able to find it again. While the data is still in the database, this is a loss of the post from a practical standpoint. This change introduces an exception where the first time a collaborative session is saving draft edits, the autodraft is promoted to a real draft post, making it visible in the post list. Developed in: WordPress/gutenberg#77865 Backport in: WordPress/wordpress-develop#11691 Trac ticket: https://core.trac.wordpress.org/ticket/65138 Co-authored-by: danluu <danluu@git.wordpress.org> Co-authored-by: dmsnell <dmsnell@git.wordpress.org> Source: WordPress/gutenberg@6f328a8
…sions to avoid post loss. When an RTC session in the editor saves edits on a draft post, they always save into an autosave revision. This prevents a sync but where storing to the post itself would lead to duplicate edits. This was set in wordpress/gutenberg commit 9df142b839320316b406ee1a02e23704d42f8719. However, there is an exception to this rule: the first session to save edits needs to promote the autosave revision into a real draft post, if a post doesn’t exist. The reason is that WordPress hides autosaves. That is, multiple people could be editing a draft post and then once they close it, never be able to find it again. While the data is still in the database, this is a loss of the post from a practical standpoint. This change introduces an exception where the first time a collaborative session is saving draft edits, the autodraft is promoted to a real draft post, making it visible in the post list. AI Disclaimer: This bug was detected in a fuzzing system built by AI models and the fix was first proposed by an AI model. Developed in: WordPress/gutenberg#77865 Discussed in: https://core.trac.wordpress.org/ticket/65138 See also: WordPress/gutenberg#77716 Follow-up to [61680]. Props danluu, dmsnell. See #65138. git-svn-id: https://develop.svn.wordpress.org/trunk@62311 602fd350-edb4-49c9-b593-d223f7449a82
…sions to avoid post loss. When an RTC session in the editor saves edits on a draft post, they always save into an autosave revision. This prevents a sync but where storing to the post itself would lead to duplicate edits. This was set in wordpress/gutenberg commit 9df142b839320316b406ee1a02e23704d42f8719. However, there is an exception to this rule: the first session to save edits needs to promote the autosave revision into a real draft post, if a post doesn’t exist. The reason is that WordPress hides autosaves. That is, multiple people could be editing a draft post and then once they close it, never be able to find it again. While the data is still in the database, this is a loss of the post from a practical standpoint. This change introduces an exception where the first time a collaborative session is saving draft edits, the autodraft is promoted to a real draft post, making it visible in the post list. AI Disclaimer: This bug was detected in a fuzzing system built by AI models and the fix was first proposed by an AI model. Developed in: WordPress/gutenberg#77865 Discussed in: https://core.trac.wordpress.org/ticket/65138 See also: WordPress/gutenberg#77716 Follow-up to [61680]. Props danluu, dmsnell. See #65138. Built from https://develop.svn.wordpress.org/trunk@62311 git-svn-id: http://core.svn.wordpress.org/trunk@61591 1a063a9b-81f0-0310-95a4-ce76da25c4cd
|
Backport merged into |
This is part of an AI fuzzing project, where an AI wrote a fuzzer and then triages bugs from the fuzzer and creates fixes. See #77716 for the tracking issue. As of this writing, there have been no known false positives from this project, but there have been some issues, which are documented in #77716. I expect we’ll see false positives at some point (and may even have one that’s been filed in a PR that hasn’t been inspected by a code owner yet).
What?
Users can lose their post and saved revisions. See this video for a repro (apologies for the long video; the video is long because it avoids doing any kind of artificial event injection, tricks with time, etc.):
rtc-autodraft-autosave-loss-trunk.mp4
BEGIN AI GENERATED TEXT
When real-time collaboration (RTC) is enabled, a brand-new post opened via
post-new.phpstarts as anauto-draft. If the user types a title and body and then the first server autosave runs, the parent post remains anauto-draftwith titleAuto Draftand empty content. The user's edits are stored only in an autosave revision.That is a content-loss bug for normal users because
auto-draftposts are hidden from the normal Drafts and Revisions recovery flows. A user who closes the editor or loses the URL has no ordinary UI path to rediscover the work, even though an autosave revision exists in the database.The bug is user-triggerable with normal actions:
No network fault, request stubbing, synthetic browser state, or direct database mutation is required.
How the bug was introduced
The relevant code is
Gutenberg_REST_Autosaves_Controller::create_item()inlib/compat/wordpress-7.0/class-gutenberg-rest-autosaves-controller.php. It computes:Then it only uses the parent-updating
wp_update_post()path when RTC is disabled:This behavior was introduced by WordPress/gutenberg#75105, commit
9df142b8393, "Real-time collaboration: Always target autosave revision." The PR fixed a real RTC bug: for existing draft posts, the original author could autosave directly into the saved post while other collaborators wrote autosave revisions. That made the saved post diverge from the persisted CRDT document and could duplicate content on reload.The regression is that #75105 collapsed two different states into one
$is_draftbranch:draft: a visible, discoverable post. RTC should usually avoid direct parent writes on autosave to prevent the CRDT divergence fixed by Real-time collaboration: Always target autosave revision #75105.auto-draft: a hidden placeholder. The first autosave has historically promoted it to a visibledraft; otherwise the user has no normal recovery path.Follow-up PRs preserved or moved the same behavior rather than introducing the loss independently:
69699955ed0, moved the RTC PHP code from experimental sync code tolib/compat/wordpress-7.0/.056d9335b5b, renamed the RTC option.9afdce8c0cc, renamed the option again towp_collaboration_enabled.4f4a7b18306, added theWP_ALLOW_COLLABORATIONbackport gate. This is not causal but affects whether the buggy path is reachable.The older core behavior, also described in WordPress/gutenberg#75751, was that an autosave by the author for a
draftorauto-draftcould update the parent post directly. That behavior was unsafe for RTC on existing drafts, but it was also the mechanism that made a newauto-draftdiscoverable.Why this is distinct from similar bugs
This is not the same as the duplication bug fixed by #75105. That bug made the saved post too far ahead of the persisted CRDT document, so reloads could duplicate content. This bug leaves the saved parent too far behind: the autosaved content exists only in an autosave revision whose parent is still hidden.
This is not the local autosave refresh bug fixed by #23928. That older bug was about sessionStorage keys for unsaved auto-draft recovery after refresh. This one occurs after a successful server autosave and persists in the database.
This is not the draft/auto-draft dirty-state bug fixed by #76624. That bug was about editor dirty-state cleanup after autosaving. Here the parent database row remains
auto-draft, so the post is not discoverable as a draft.This is related to, but distinct from, the RTC test expectation updates in #75751 and #75733. Those PRs recognized that RTC changed autosave behavior, and #75751 explicitly mentions avoiding CRDT persistence for auto-drafts. They did not assert the user-facing invariant that the first server autosave of a new post must leave a recoverable draft.
This is not primarily a multi-user race. A single browser session can trigger it. Multi-user RTC can make consequences worse, but is not required.
This is not the broader
persistCRDTDocfull-record write issue in #77049. That issue is about invalid REST payloads when persisting_crdt_document; this issue is about the autosave controller choosing an autosave revision instead of promoting an auto-draft parent.Initial fix plan
post-new.php, types title/content, waits for the real automatic autosave request, and verifies the post is a visibledraftwith the typed content.draftfromauto-draftinGutenberg_REST_Autosaves_Controller::create_item().draftposts under RTC: autosaves should continue to target autosave revisions, not the parent post.auto-draftunder RTC, promote the parent todrafton first autosave so the post becomes discoverable.Audit of the initial plan
These are perspective audits, not statements by the named people.
Linus Torvalds lens
The plan has the right instinct but is too hand-wavy around state transitions. The bug exists because the code used a sloppy boolean,
$is_draft, for two states with different invariants. The fix should encode the state machine explicitly:auto-draftfirst autosave is a promotion transition.draftautosave is a revision transition under RTC.Avoid spreading special cases into unrelated client code unless server-side behavior forces it. The server owns post status and discoverability, so the controller should make the post-state decision.
Kyle Kingsbury / Jepsen lens
The plan needs an invariant, not just examples. The invariant is: after any successful server autosave response for user-authored new-post content, either the content is reachable through a normal user recovery path or the response must not be considered a successful durability signal.
Tests should check externally visible state, not only response codes:
The plan should also test concurrency around the promotion boundary: two tabs or peers editing the same auto-draft should not create two visible drafts, orphan autosaves, or divergent CRDT rooms.
Dan Luu lens
The plan should account for why this escaped. Existing tests were close to the bug but asserted implementation details or changed expectations after RTC became default. They did not encode the user-support invariant: if a user waits for "Saved" or autosave has completed, their new draft must be findable later.
The fix should include a targeted product-level regression test, not just a unit test. It should also document the support implication: "content exists in an autosave revision" is not an acceptable recovery story if the parent remains a hidden auto-draft and the user does not have the edit URL.
Updated fix plan
$is_draftbranch to explicit state checks:$is_auto_draft = 'auto-draft' === $post->post_status;$is_draft = 'draft' === $post->post_status;create_item(), allow the normal parent update path forauto-draftpromotion even when RTC is enabled, subject to the same author and lock checks that core uses for direct parent autosaves.draftposts so Real-time collaboration: Always target autosave revision #75105 does not regress._crdt_documentand reload/join can reproduce Real-time collaboration: Always target autosave revision #75105-style duplication, add the smallest client-side follow-up: when an autosave response updates the parent fromauto-drafttodraft, treat that one transition as save-like for the sync manager and persist a matching CRDT document. Do not generalize this to existing draft autosaves.auto-draftis not grouped withdraftunder RTC. The comment should point to the user-visible discoverability invariant, not just the implementation mechanics.Expected fix shape
The likely server-side shape is:
This is intentionally conservative: it changes only the
auto-draftRTC transition while preserving the existing RTC revision path for visible drafts.The open question is whether the auto-draft promotion path also needs an immediate
_crdt_documentpersistence step. That should be decided by the reload/join no-duplication test, not by assumption.END AI GENERATED TEXT