Worker threads: reject pending RPC calls on worker failure or termination - #79955
Conversation
…tion comctx only settles a call promise when a response message arrives from the worker, so a worker that crashed, failed to initialize, or was terminated mid-call left its promises pending forever. In the upload queue this surfaced as an image stuck on its blob: placeholder with the progress spinner, no error surfaced, and the editor save lock never released. Track pending call rejecters per wrapped worker so that: - terminate() rejects pending calls, as its docblock already promised. - The worker's error/messageerror events reject pending calls. - Calls made after a failure/termination reject immediately instead of hanging, until the consumer recreates the worker. Fixes #79953
|
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. |
|
Size Change: +667 B (+0.01%) Total Size: 7.72 MB 📦 View Changed
|
|
Flaky tests detected in 3e63024. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/28952781567
|
If the underlying remote call threw synchronously or returned a non-thenable, the rejecter would stay in pendingRejects until the next worker failure. Route the call through Promise.resolve() so cleanup always runs.
…celItem When the vips or video-conversion worker has crashed, every call on its remote rejects - including the cancellation issued by cancelItem itself. That rejection escaped cancelItem before onError fired and before the item was removed, so a worker crash mid-transcode left the upload stuck in the queue, the save lock held, and the dead worker never recycled. Catch rejections from vipsCancelOperations and cancelGifToVideoOperations so the cancellation flow always completes: the error surfaces to the user, the item leaves the queue, and emptying the queue tears down the dead worker so the next upload gets a fresh one. Adds unit tests for both rejecting cancel calls and an e2e test that crashes the vips worker mid-upload and asserts the editor surfaces the error, releases the save lock, and recovers on the next upload.
andrewserong
left a comment
There was a problem hiding this comment.
I also wasn't able to manually test this one for some reason (I could pause workers, but Chrome didn't let me terminate any 🤷)
But, smoke testing uploading works as expected, and I didn't run into any issues looking over the code, and everything I got from passing this through Claude seemed minor/like nits. E.g.
- That the
failWorkerstate means subsequent calls for a failed worker are rejected even if the worker is still running (i.e. it'll run until it's terminated). That seems like the intention of this PR, so doesn't sound like an issue? removeListeners()is only called when terminating the worker. Is that expected?- And "Wrapping the delegate in Promise.resolve().then( () => value( ...args ) )" adds a microtask of latency. This also sounds like a non-issue to me
Given we're close to the feature freeze, I'll give this a tentative approval. But let me know if you wanted someone to be able to manually reproduce the testing steps, since it seems neither Ramon or I have been able to yet.
|
Some findings from the #80259 investigation (fix in #80260) that are relevant here: The hang in #80259 turned out to be a wasm deadlock, not a crash: a libjpeg failure killed a libvips worker pthread without unwinding, and the coordinating thread blocked forever in One concrete gap in the upload-media change: await Promise.race( [
vipsCancelOperations( id ).catch( () => {} ),
new Promise( ( resolve ) => setTimeout( resolve, 3000 ) ),
] );Silver lining: the Also FYI: #80260 touches the worker side of this package ( |
|
So, sounds like we merge this PR now(ish) and then follow-up in #80260? |
|
There was a conflict while trying to cherry-pick the commit to the wp/7.1 branch. Please resolve the conflict manually and create a PR to the wp/7.1 branch. PRs to wp/7.1 are similar to PRs to trunk, but you should base your PR on the wp/7.1 branch instead of trunk. |
|
It looks like this was missing the 7.1 backport label and was never added to the 7.1 branch. |
|
Removing the |
|
This PR was manually backported by #80421. |
This updates the pinned commit hash of the Gutenberg repository from `e73c3c481db0650183f092af157f6e42efe9ee2d` to `4997026b75c922d8a6f77a03d72ed7cad04c7073`. A full list of changes included in this commit can be found on GitHub: WordPress/gutenberg@e73c3c4...4997026 - Notes: Replace blur-deselect bookkeeping with useFocusOutside (WordPress/gutenberg#80222) - Playlist: Update @SInCE tags to 7.1.0 (WordPress/gutenberg#80317) - fix playlist block Dimensions Design (WordPress/gutenberg#80312) - UI: Backport compat overlay fixes to WordPress 7.1 (WordPress/gutenberg#80322) - Editor: allow selecting which block styles to apply globally (WordPress/gutenberg#79839) - Global Styles: Reject non-string custom CSS in the REST controller (WordPress/gutenberg#80338) - Open inspector sidebar when toggling responsive editing (WordPress/gutenberg#80307) - Client Side Media: Honor image_strip_meta and image_max_bit_depth on the client upload path (WordPress/gutenberg#80218) - Hide block style variations when state is enabled in global styles (WordPress/gutenberg#80341) - Media REST API: Fix sideload and finalize for EXIF rotated images (WordPress/gutenberg#80295) - Fix upload snackbar stuck in uploading state on server-side uploads (WordPress/gutenberg#80345) - Try fixing responsive layout in Nav block (WordPress/gutenberg#80305) - Responsive styles: Use viewport dropdown to control states for in-editor global styles sidebar (WordPress/gutenberg#80339) - RichTextControl: Replace DOM focus tracking with a single React-tree focus boundary (WordPress/gutenberg#80324) - Notes: Finish WPDS treatment for mention chips (WordPress/gutenberg#80300) - Notes: Add placeholders to the RichText fields (WordPress/gutenberg#80296) - Fix upload hang when converting long animated GIFs: decode only the first frame for still outputs (WordPress/gutenberg#80260) (WordPress/gutenberg#80342) - Device preview dropdown: use active color for device icon when responsive styles are active (WordPress/gutenberg#80346) - Fix default aspect ratio for lazy loaded Featured image (WordPress/gutenberg#80386) - Vips/upload-media: consolidate optional params into options objects (WordPress/gutenberg#80330) - Autocompleters: Don't pre-encode mention search terms (WordPress/gutenberg#80377) - Animated GIF uploads: generate sub-sizes from the first frame, matching core (WordPress/gutenberg#80268) - Custom CSS: Fix cascade order against block style variations (WordPress/gutenberg#80340) - Rich Text: Restore the selection when focus returns to the editable (WordPress/gutenberg#80396) - Notes: Arm the mention kses allowance on REST note creation (WordPress/gutenberg#80221) - Fix upload snackbar double-counting a single HEIC upload in Safari (WordPress/gutenberg#80436) - ContentEditableControl: fix invalid label association with contenteditable div (WordPress/gutenberg#80441) - Editor: Disable canvas resizing while zoomed out (WordPress/gutenberg#80391) - Fix Color Picker Cursor Shaking Issue (WordPress/gutenberg#80205) (WordPress/gutenberg#80435) - Misc fixes for WordPress-Develop 7.0 merges (WordPress/gutenberg#80444) - Style Book: Restore live global styles updates on the styles route (WordPress/gutenberg#80459) - Worker threads: reject pending RPC calls on worker failure or termination (WordPress/gutenberg#79955) (WordPress/gutenberg#80421) - Media: Add timeout and size guardrails to client-side GIF to video conversion (WordPress/gutenberg#80420) - Post Content: Use the default block appender for empty content (WordPress/gutenberg#80026) - Block Supports: Handle nested array block gap values properly (WordPress/gutenberg#80464) - Editor: Restore fixed device preview height for mobile and tablet (WordPress/gutenberg#80466) - Block Editor: Guard against non-string spacing preset values (WordPress/gutenberg#80467) - Writing flow: fully select the ancestor when a text selection crosses a nesting boundary (WordPress/gutenberg#80462) - Block Editor: Reflect inherited Global Styles values in block inspector controls (WordPress/gutenberg#80481) - Autocomplete: Reference the suggestions list with `aria-controls` and `aria-haspopup` (WordPress/gutenberg#80403) (WordPress/gutenberg#80499) - Media: Remove the redundant __heicUploadSupport flag (WordPress/gutenberg#80486) - State control - avoid tertiary variant on toggle to match style of other dropdown toggles (WordPress/gutenberg#80505) - Icons: Store the sanitized SVG content when registering an icon (WordPress/gutenberg#80508) - Fix `useHomeEnd` on tabs in mac testing (WordPress/gutenberg#80374) - Playlist: Fix playback of tracks served without CORS headers (WordPress/gutenberg#80533) - Redirect editing events to extension handlers under editableRoot (WordPress/gutenberg#80287) - Writing flow: fully select the items when a selection extends down into a nested item (WordPress/gutenberg#80492) - Global Styles panels: fix wrong preset committed and shown when two color presets share a hex (WordPress/gutenberg#80497) - Replaces the `title` attributes used by revision inline diff annotations with `aria-describedby` (WordPress/gutenberg#80440) - Notes: Remove "Add note" from the inline styles dropdown (WordPress/gutenberg#80531) - Global Styles: Resolve per-level heading element styles in block inspector controls (WordPress/gutenberg#80495) - Notes: Render @ mentions as span chips and narrow the kses class allowance (WordPress/gutenberg#80528) - Revisions: Specify block level diff status via aria-label (WordPress/gutenberg#77779) - Backport from Core: improve icon name unit tests (WordPress/gutenberg#80552) - Device type preview: fix collapsing to content height (WordPress/gutenberg#80553) - Wrap notices in ThemeProvider with 0 corner radius (WordPress/gutenberg#80557) - Global Styles: Limit the inherited value treatment to the Gutenberg plugin (WordPress/gutenberg#80555) - Fix crashes when manipulating locked blocks (WordPress/gutenberg#80509) - Notes: align floating threads with their inline marker (WordPress/gutenberg#79877) Props wildworks. See #65529. git-svn-id: https://develop.svn.wordpress.org/trunk@62824 602fd350-edb4-49c9-b593-d223f7449a82
This updates the pinned commit hash of the Gutenberg repository from `e73c3c481db0650183f092af157f6e42efe9ee2d` to `4997026b75c922d8a6f77a03d72ed7cad04c7073`. A full list of changes included in this commit can be found on GitHub: WordPress/gutenberg@e73c3c4...4997026 - Notes: Replace blur-deselect bookkeeping with useFocusOutside (WordPress/gutenberg#80222) - Playlist: Update @SInCE tags to 7.1.0 (WordPress/gutenberg#80317) - fix playlist block Dimensions Design (WordPress/gutenberg#80312) - UI: Backport compat overlay fixes to WordPress 7.1 (WordPress/gutenberg#80322) - Editor: allow selecting which block styles to apply globally (WordPress/gutenberg#79839) - Global Styles: Reject non-string custom CSS in the REST controller (WordPress/gutenberg#80338) - Open inspector sidebar when toggling responsive editing (WordPress/gutenberg#80307) - Client Side Media: Honor image_strip_meta and image_max_bit_depth on the client upload path (WordPress/gutenberg#80218) - Hide block style variations when state is enabled in global styles (WordPress/gutenberg#80341) - Media REST API: Fix sideload and finalize for EXIF rotated images (WordPress/gutenberg#80295) - Fix upload snackbar stuck in uploading state on server-side uploads (WordPress/gutenberg#80345) - Try fixing responsive layout in Nav block (WordPress/gutenberg#80305) - Responsive styles: Use viewport dropdown to control states for in-editor global styles sidebar (WordPress/gutenberg#80339) - RichTextControl: Replace DOM focus tracking with a single React-tree focus boundary (WordPress/gutenberg#80324) - Notes: Finish WPDS treatment for mention chips (WordPress/gutenberg#80300) - Notes: Add placeholders to the RichText fields (WordPress/gutenberg#80296) - Fix upload hang when converting long animated GIFs: decode only the first frame for still outputs (WordPress/gutenberg#80260) (WordPress/gutenberg#80342) - Device preview dropdown: use active color for device icon when responsive styles are active (WordPress/gutenberg#80346) - Fix default aspect ratio for lazy loaded Featured image (WordPress/gutenberg#80386) - Vips/upload-media: consolidate optional params into options objects (WordPress/gutenberg#80330) - Autocompleters: Don't pre-encode mention search terms (WordPress/gutenberg#80377) - Animated GIF uploads: generate sub-sizes from the first frame, matching core (WordPress/gutenberg#80268) - Custom CSS: Fix cascade order against block style variations (WordPress/gutenberg#80340) - Rich Text: Restore the selection when focus returns to the editable (WordPress/gutenberg#80396) - Notes: Arm the mention kses allowance on REST note creation (WordPress/gutenberg#80221) - Fix upload snackbar double-counting a single HEIC upload in Safari (WordPress/gutenberg#80436) - ContentEditableControl: fix invalid label association with contenteditable div (WordPress/gutenberg#80441) - Editor: Disable canvas resizing while zoomed out (WordPress/gutenberg#80391) - Fix Color Picker Cursor Shaking Issue (WordPress/gutenberg#80205) (WordPress/gutenberg#80435) - Misc fixes for WordPress-Develop 7.0 merges (WordPress/gutenberg#80444) - Style Book: Restore live global styles updates on the styles route (WordPress/gutenberg#80459) - Worker threads: reject pending RPC calls on worker failure or termination (WordPress/gutenberg#79955) (WordPress/gutenberg#80421) - Media: Add timeout and size guardrails to client-side GIF to video conversion (WordPress/gutenberg#80420) - Post Content: Use the default block appender for empty content (WordPress/gutenberg#80026) - Block Supports: Handle nested array block gap values properly (WordPress/gutenberg#80464) - Editor: Restore fixed device preview height for mobile and tablet (WordPress/gutenberg#80466) - Block Editor: Guard against non-string spacing preset values (WordPress/gutenberg#80467) - Writing flow: fully select the ancestor when a text selection crosses a nesting boundary (WordPress/gutenberg#80462) - Block Editor: Reflect inherited Global Styles values in block inspector controls (WordPress/gutenberg#80481) - Autocomplete: Reference the suggestions list with `aria-controls` and `aria-haspopup` (WordPress/gutenberg#80403) (WordPress/gutenberg#80499) - Media: Remove the redundant __heicUploadSupport flag (WordPress/gutenberg#80486) - State control - avoid tertiary variant on toggle to match style of other dropdown toggles (WordPress/gutenberg#80505) - Icons: Store the sanitized SVG content when registering an icon (WordPress/gutenberg#80508) - Fix `useHomeEnd` on tabs in mac testing (WordPress/gutenberg#80374) - Playlist: Fix playback of tracks served without CORS headers (WordPress/gutenberg#80533) - Redirect editing events to extension handlers under editableRoot (WordPress/gutenberg#80287) - Writing flow: fully select the items when a selection extends down into a nested item (WordPress/gutenberg#80492) - Global Styles panels: fix wrong preset committed and shown when two color presets share a hex (WordPress/gutenberg#80497) - Replaces the `title` attributes used by revision inline diff annotations with `aria-describedby` (WordPress/gutenberg#80440) - Notes: Remove "Add note" from the inline styles dropdown (WordPress/gutenberg#80531) - Global Styles: Resolve per-level heading element styles in block inspector controls (WordPress/gutenberg#80495) - Notes: Render @ mentions as span chips and narrow the kses class allowance (WordPress/gutenberg#80528) - Revisions: Specify block level diff status via aria-label (WordPress/gutenberg#77779) - Backport from Core: improve icon name unit tests (WordPress/gutenberg#80552) - Device type preview: fix collapsing to content height (WordPress/gutenberg#80553) - Wrap notices in ThemeProvider with 0 corner radius (WordPress/gutenberg#80557) - Global Styles: Limit the inherited value treatment to the Gutenberg plugin (WordPress/gutenberg#80555) - Fix crashes when manipulating locked blocks (WordPress/gutenberg#80509) - Notes: align floating threads with their inline marker (WordPress/gutenberg#79877) Props wildworks. See #65529. Built from https://develop.svn.wordpress.org/trunk@62824 git-svn-id: http://core.svn.wordpress.org/trunk@62104 1a063a9b-81f0-0310-95a4-ce76da25c4cd
Fixes #79953
What?
Makes worker RPC promises in
@wordpress/worker-threadssettle when the underlying Web Worker fails or is terminated, instead of hanging forever.Why?
comctx (the RPC layer used by
wrap()) only settles a call promise when a response message arrives from the worker, andwrap()runs withheartbeatCheck: false. If the vips worker dies, fails to initialize, or is terminated while an operation is in flight, the promise for that operation never settles. The upload queue item then stays in its current operation forever:blob:placeholder and a progress spinner indefinitely.onErrorpath never runs).isUploading()staystrue, so the editor's upload save lock keeps the post unsaveable.The worker is terminated and recreated frequently (
removeItemterminates it whenever the queue empties, and it is recycled every 50 operations), so failure windows occur many times per editing session. See #79953 for the full analysis.How?
In
packages/worker-threads/src/main-thread.ts,wrap()now tracks the rejecter of every in-flight call:terminate()rejects all pending calls, as its docblock already promised ("any pending calls will be rejected") but the implementation never did.error/messageerrorlisteners on the worker reject all pending calls when the worker crashes, fails script load/WASM instantiation, or delivers an undeserializable message.This closes the failure loop with the existing queue logic: a rejected vips call cancels the queue item and surfaces
onError, the queue drains, the empty-queue cleanup terminates the dead worker, and the next upload lazily creates a fresh one (getWorkerAPI()in@wordpress/vips). Both the vips and video-conversion workers benefit.Per review feedback,
cancelItemin@wordpress/upload-mediaalso needed a change: it awaitsvipsCancelOperations()/cancelGifToVideoOperations(), and with a dead worker those cancel calls themselves now reject, which previously aborted the cancellation flow beforeonErrorfired and the item was removed. Those calls are now best-effort (.catch( () => {} )), which keeps the recovery loop intact end to end.The remaining hardening ideas from #79953 (per-call timeout for a wedged-but-alive worker, debounced empty-queue termination, awaiting
mediaDeleteincancelItem) are intentionally left as follow-ups to keep this focused.Testing Instructions
npm run test:unit packages/worker-threadsThe 5 new test cases hang (5s timeout each) without the fix and pass with it: worker
errorevent rejects pending calls,messageerrorrejects pending calls, calls after a worker error reject immediately,terminate()rejects pending calls, and calls after termination reject immediately.npm run test:unit packages/upload-media— includes two new tests assertingcancelItemstill firesonErrorand removes the item when the worker cancel calls reject (both fail without the best-effort change).npm run test:e2e -- test/e2e/specs/editor/various/client-side-media-processing.spec.js— includes a new test that crashes the vips worker mid-upload and asserts the error snackbar appears, the queue empties, the save lock releases, and a follow-up upload succeeds with a fresh worker.Manual verification of the end-to-end behavior:
chrome://inspect/#workers) and terminate the blob worker, or simulate a crash.blob:placeholder forever and the post can't be saved. With this PR the upload fails with an error notice and the save lock releases.