Skip to content

Fix editor exit hang when GutenbergView never finishes loading#22882

Open
jkmassel wants to merge 1 commit into
trunkfrom
jkmassel/issue-22878
Open

Fix editor exit hang when GutenbergView never finishes loading#22882
jkmassel wants to merge 1 commit into
trunkfrom
jkmassel/issue-22878

Conversation

@jkmassel
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel commented May 21, 2026

Summary

If GutenbergView never finishes loading (e.g. an upstream 404 on wp-block-editor/v1/settings, network failure mid-init, or any other case where the JS bridge never becomes available), the editor activity gets stuck — back press doesn't dismiss it, the "saving" progress dialog stays open, and the only way out is to force-stop the app.

Fixes #22878.

Root cause

GutenbergKitEditorFragment.getTitleAndContent blocks on CountDownLatch.await() with no timeout. The save-on-exit flow (savePostAndOptionallyFinishupdateAndSavePostAsyncOnEditorExitupdateAndSavePostAsyncupdateFromEditorgetTitleAndContent) sits on this latch waiting for TitleAndContentCallback.onResult to fire from JS. When GutenbergView hasn't finished loading, the bridge call is dropped silently — GBKit internally guards on its private isEditorLoaded field and logs "You can't change the editor content until it has loaded". latch.countDown() never fires; the activity never finishes.

Introduced incidentally by #22764 ("Upgrade GutenbergKit from v0.11.1 to v0.15.2"), which switched the bridge call from sync to async-with-latch.

Fix

GBKit doesn't currently expose a public ready-state property — only the setEditorDidBecomeAvailable {} callback. Three layers, all host-side.

1. Track ready state in the fragment

@Volatile var editorReady flipped inside the setEditorDidBecomeAvailable {} listener we already register; reset to false in onCreateView so a stale true doesn't leak across config changes. @Volatile is required because the field is written from the JS bridge thread and read from bgDispatcher (via EditPostRepository.updateAsync). Exposed publicly via isEditorReady().

2. Bound the latch

Replace latch.await() with latch.await(5, TimeUnit.SECONDS). On timeout (or InterruptedException), getTitleAndContent throws EditorFragmentNotAddedException rather than returning a sentinel empty Pair.

3. Short-circuit the save flow before it mutates the PostModel

GutenbergKitActivity.updateFromEditor checks isEditorReady() before calling getTitleAndContent. When false (or when the timeout path throws), it returns UpdateFromEditor.Failed(...). StorePostViewModel.updatePostObjectWithUI treats Failed as a no-op — updatePostContentNewEditor is never called, the in-memory PostModel is not mutated, _postChanged does not fire, and the auto-save observer at GutenbergKitActivity.kt:1158-1162 does not run.

This last step is load-bearing. An earlier draft of this PR returned Pair("", "") from getTitleAndContent on stall, on the assumption that "empty post → no save attempted." That assumption was wrong. _postChanged fires whenever the in-memory PostModel changes, and editPostRepository.postChanged.observe calls storePostViewModel.savePostToDb regardless of shouldSavePost(). For an existing draft, the in-memory overwrite to empty would have been persisted to SQLite, silently wiping the user's content.

Incidental: exception-class mismatch in updateFromEditor's catch

The pre-existing catch was catch (e: EditorFragmentAbstract.EditorFragmentNotAddedException), but GutenbergKitEditorFragment throws GutenbergKitEditorFragmentBase.EditorFragmentNotAddedException — two unrelated inner classes sharing the same simple name, descending separately from Exception. The catch was dead code. Changed to match the actually-thrown class so the new timeout-throw is caught.

Net behaviour

Editor stall → Failed returned → no PostModel mutation → no DB write → activity finishes via ActivityFinishState.CANCELLED within ~5s. Existing draft on disk is preserved.

The underlying 404 / route-detection asymmetry on Atomic sites is a separate concern, owned by #22812 / capability-detection territory. This PR is specifically about the host's failure mode when the editor doesn't load, regardless of why it didn't load.

Test plan

  • On WP.com simple site, open a post in the editor, type, and exit — content saves and the activity finishes as before.
  • On an Atomic site (I suggest using automatticwidgets.wpcomstaging.com), the editor stalls with Theme Styles and Third-Party Blocks enabled (you'll need to turn these on in Site Settings to see it), open a post in the editor, wait for the stall, press the back button or X — the activity finishes within ~5 seconds instead of hanging indefinitely.
  • Open an existing draft with non-empty content on a stalling site, wait for the stall, exit, then reopen the post in a working session — the original content is still there (not overwritten with empty).
  • No savePostToDb call is made when exiting from a stalled session (verify in logs).

Related

@jkmassel jkmassel marked this pull request as draft May 21, 2026 15:59
@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented May 21, 2026

1 Warning
⚠️ This PR is assigned to the milestone 26.8. The due date for this milestone has already passed.
Please assign it to a milestone with a later deadline or check whether the release for this milestone has already been finished.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented May 21, 2026

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr22882-8e8b670
Build Number1488
Application IDcom.jetpack.android.prealpha
Commit8e8b670
Installation URL0d1gsvmap41u8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented May 21, 2026

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr22882-8e8b670
Build Number1488
Application IDorg.wordpress.android.prealpha
Commit8e8b670
Installation URL0smsge74i4plg
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@jkmassel jkmassel force-pushed the jkmassel/issue-22878 branch from fac8211 to 785ecd8 Compare May 21, 2026 16:51
@wpmobilebot
Copy link
Copy Markdown
Contributor

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

Track ready state from `setEditorDidBecomeAvailable` and short-circuit
the save-on-exit flow to `UpdateFromEditor.Failed` when the editor
isn't ready. Bound the bridge latch with a 5s timeout that also routes
through `Failed`. Both paths skip mutating the `PostModel`, so the
user's existing draft is preserved when the editor stalls.

Fixes #22878
@jkmassel jkmassel force-pushed the jkmassel/issue-22878 branch from 785ecd8 to 8e8b670 Compare May 21, 2026 18:28
@jkmassel jkmassel requested a review from nbradbury May 21, 2026 18:40
@jkmassel jkmassel self-assigned this May 21, 2026
@jkmassel jkmassel added this to the 26.8 milestone May 21, 2026
@jkmassel jkmassel marked this pull request as ready for review May 21, 2026 18:43
Copy link
Copy Markdown
Contributor

@nbradbury nbradbury left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM :shipit:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Editor exit hangs indefinitely when GutenbergView never finishes loading

4 participants