Skip to content

Conversation

@adamziel
Copy link
Contributor

@adamziel adamziel commented May 6, 2020

Description

This PR enables saving the entire navigation using a single HTTP request.

The current behavior is sending one request per change at the moment. If you e.g. add 20 menu items, update 17 and remove 10, it will result in 47 http requests. This is pretty fragile and would easily break under a number of scenarios - validation errors, network failures, accidentally closing the browser, and so on.

The proposed implementation is as follows:

  • On save, the browser sends the entire tree to the API
  • The API validates the entire input first and then processes all the deletes/updates in a loop.
  • Implementing batch inserts is not trivial so the batch processing endpoint does not deal with inserts. Instead, when a new navigation link is created in the browser, we send an XHR to create a placeholder menu item. This makes it possible for the API to only deal with updates.
  • Menu order and parent/child relationships are not sent over the wire, instead they are inferred from the tree.

Other notes:

  • Processing the batch may fail in the middle. This leaves the stored data in a broken state. The risk of this happening is much lower than with the current "XHR per change" approach. This part is the same as on the existing nav-menus.php screen.
  • To make the next version more resilient, we could explore rolling back all changes on failure. A few possible approaches are: using MySQL transactions and refreshing the caches on rollback, keeping track of the delta and applying an inverse diff.

How has this been tested?

Tested locally

Types of changes

New feature (non-breaking change which adds functionality)

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR.

@adamziel adamziel self-assigned this May 6, 2020
@adamziel adamziel changed the title First stab at saving the entire menu at once Save entire navigation menu at once May 6, 2020
@github-actions
Copy link

github-actions bot commented May 6, 2020

Size Change: +661 B (0%)

Total Size: 834 kB

Filename Size Change
build/annotations/index.js 3.62 kB +2 B (0%)
build/block-editor/index.js 105 kB -3 B (0%)
build/block-library/index.js 118 kB +1 B
build/components/index.js 182 kB -2 B (0%)
build/core-data/index.js 11.4 kB +2 B (0%)
build/data-controls/index.js 1.29 kB -1 B
build/edit-navigation/index.js 6.44 kB +664 B (10%) ⚠️
build/edit-post/index.js 28.1 kB +2 B (0%)
build/edit-site/index.js 12 kB -1 B
build/editor/index.js 44.3 kB -5 B (0%)
build/list-reusable-blocks/index.js 3.13 kB +1 B
build/media-utils/index.js 5.29 kB -1 B
build/server-side-render/index.js 2.68 kB +2 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.02 kB 0 B
build/api-fetch/index.js 3.39 kB 0 B
build/autop/index.js 2.83 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/index.js 6.59 kB 0 B
build/block-directory/style-rtl.css 764 B 0 B
build/block-directory/style.css 764 B 0 B
build/block-editor/style-rtl.css 10.8 kB 0 B
build/block-editor/style.css 10.8 kB 0 B
build/block-library/editor-rtl.css 7.19 kB 0 B
build/block-library/editor.css 7.19 kB 0 B
build/block-library/style-rtl.css 7.48 kB 0 B
build/block-library/style.css 7.48 kB 0 B
build/block-library/theme-rtl.css 683 B 0 B
build/block-library/theme.css 685 B 0 B
build/block-serialization-default-parser/index.js 1.88 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/blocks/index.js 48.1 kB 0 B
build/components/style-rtl.css 17.1 kB 0 B
build/components/style.css 17 kB 0 B
build/compose/index.js 6.67 kB 0 B
build/data/index.js 8.42 kB 0 B
build/date/index.js 5.47 kB 0 B
build/deprecated/index.js 772 B 0 B
build/dom-ready/index.js 569 B 0 B
build/dom/index.js 3.11 kB 0 B
build/edit-navigation/style-rtl.css 709 B 0 B
build/edit-navigation/style.css 708 B 0 B
build/edit-post/style-rtl.css 12.2 kB 0 B
build/edit-post/style.css 12.2 kB 0 B
build/edit-site/style-rtl.css 5.22 kB 0 B
build/edit-site/style.css 5.22 kB 0 B
build/edit-widgets/index.js 7.87 kB 0 B
build/edit-widgets/style-rtl.css 4.69 kB 0 B
build/edit-widgets/style.css 4.69 kB 0 B
build/editor/editor-styles-rtl.css 425 B 0 B
build/editor/editor-styles.css 428 B 0 B
build/editor/style-rtl.css 5.07 kB 0 B
build/editor/style.css 5.08 kB 0 B
build/element/index.js 4.65 kB 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/index.js 7.63 kB 0 B
build/format-library/style-rtl.css 502 B 0 B
build/format-library/style.css 502 B 0 B
build/hooks/index.js 2.13 kB 0 B
build/html-entities/index.js 622 B 0 B
build/i18n/index.js 3.56 kB 0 B
build/is-shallow-equal/index.js 711 B 0 B
build/keyboard-shortcuts/index.js 2.51 kB 0 B
build/keycodes/index.js 1.94 kB 0 B
build/list-reusable-blocks/style-rtl.css 226 B 0 B
build/list-reusable-blocks/style.css 226 B 0 B
build/notices/index.js 1.79 kB 0 B
build/nux/index.js 3.4 kB 0 B
build/nux/style-rtl.css 616 B 0 B
build/nux/style.css 613 B 0 B
build/plugins/index.js 2.56 kB 0 B
build/primitives/index.js 1.5 kB 0 B
build/priority-queue/index.js 789 B 0 B
build/redux-routine/index.js 2.85 kB 0 B
build/rich-text/index.js 14.8 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/token-list/index.js 1.28 kB 0 B
build/url/index.js 4.02 kB 0 B
build/viewport/index.js 1.84 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

@adamziel adamziel force-pushed the add/navigation-atomic-save branch from ca4e8b2 to 027ede9 Compare May 7, 2020 09:04
@adamziel
Copy link
Contributor Author

adamziel commented May 7, 2020

CCing @talldan @draganescu @tellthemachines @spacedmonkey @noisysocks since I know you're all working on saving the navigation in one form or another. This PR doesn't really work just yet and needs a lot more work before it's shippable, but it's pretty close to being a functional Proof of Concept. Thoughts and discussions appreciated!

I'm not really sure about the request mocking approach - it made the code short as there is no easy way to reuse parts of the codebase. I think it would work as an experiment, but a proper refactoring would be probably a good way to go long-term.

@spacedmonkey
Copy link
Member

I am honestly like the idea of this and think the effect put into this. However, I am not sure this PR is going to work.

Sites with 200+ menu items, would mean would mean 200+ of mysql updates to the post table and a more update to the post meta table. Not to mention update to caches and any filters that run on update. This means that likely the a request will timeout or error out.

As WordPress doesn't really have a system of transactioning and rolling back, with would mean half updated menu items, which we are trying to avoid.

Also validation would need to happen before any update is done. How would you report back if they were 100 errors in the request?

Of the proof of concerpt of this end was done in the feature plugin. This maybe the best place for this discussion going forward.

@noisysocks
Copy link
Member

Sites with 200+ menu items, would mean would mean 200+ of mysql updates to the post table and a more update to the post meta table. Not to mention update to caches and any filters that run on update. This means that likely the a request will timeout or error out.

How does the existing nav-menus.php screen in WP Admin get around this?

Copy link
Member

Choose a reason for hiding this comment

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

MySQL TRANSACTION are not supported by all hosts. This would require a change to WordPress's min requirements. Also the fact isn't fail gracefully here, is not great.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, I want to get it to a state where it uses MySQL transactions when they're available, and doesn't use them when they're not. I'm exploring:

  1. Bulk validation before performing any sql operations
  2. Only performing SQL queries for menu items that are actually changed, not for all of them

Copy link
Member

Choose a reason for hiding this comment

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

This is talking about a sizable change to WordPress core. I think this should really be discussed in a ticket on Trac.

Copy link
Member

Choose a reason for hiding this comment

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

What about if the request times out, what happens? How about all caches that were updated? How are these rolled back?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@spacedmonkey still exploring - with the current "brute force" way it takes 40s to save 500 menu items which isn't terrible - I think I can get it down to 2-5 with some tweaks.

How about all caches that were updated? How are these rolled back?

Good question - I'm not sure yet, I can imagine a scenario where it's so non-trivial we'll have to abandon this approach for the time being. I have some ideas like disabling any hooks, performing SQL queries, and only notifying them if everything worked properly. Or maybe validating first would be sufficient in most cases?

Copy link
Member

Choose a reason for hiding this comment

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

Disabling cache invalidation and hooks, waiting for updates to complete, is going to generate a lot of error IMO. The amount of testing to get this into core would far to high and really require a skilled core commiter to comit to this way of doing before we continue with this course of action.

@adamziel adamziel force-pushed the add/navigation-atomic-save branch from 523239f to 2223fd9 Compare May 11, 2020 15:05
@adamziel
Copy link
Contributor Author

Of the proof of concerpt of this end was done in the feature plugin. This maybe the best place for this discussion going forward.

@spacedmonkey thank you! I'll check this out tomorrow and follow up

@draganescu
Copy link
Contributor

@spacedmonkey do you have an example of a website using over 100 menu items? I don't ask it as proof, I too believe it exists, but I would love to see one as a way to understand the context of that in the wild b/c it might affect other scenarios as well such as converting from existing menus to nav block menus.

I can think of a "directory" type of website where people display links using nested nav menus which are programatically maintained. Other than that ... no idea.

@adamziel
Copy link
Contributor Author

Of the proof of concerpt of this end was done in the feature plugin. This maybe the best place for this discussion going forward.

@spacedmonkey do you mean a proof of concept of bulk validation? Would you mind pointing me to the specific line/pr? I can't find it unfortunately.

@spacedmonkey
Copy link
Member

@spacedmonkey do you have an example of a website using over 100 menu items?

Of the top of my head

@spacedmonkey
Copy link
Member

@spacedmonkey do you mean a proof of concept of bulk validation? Would you mind pointing me to the specific line/pr? I can't find it unfortunately.

The feature plugin can be found here. https://github.com/WP-API/menus-endpoints Work on this endpoint has been going on for over a year on this repo. A change of this later size, should really happen there where the REST API team can approve PRs and discuss the issue.

@adamziel
Copy link
Contributor Author

@spacedmonkey ah that's what you meant, sorry - I thought you mean there's a prototype of bulk validation somewhere in that repo. I think it makes sense! I'll recycle this PR for a bit more just because it makes the development easy, and then will switch over to the feature plugin repo and initiate a proper PR there.

@adamziel adamziel force-pushed the add/navigation-atomic-save branch from 2223fd9 to ca9327f Compare May 12, 2020 14:33
@adamziel
Copy link
Contributor Author

adamziel commented May 12, 2020

I got it to a place where the validation is performed in bulk before saving and the endpoint reliably persists the submitted tree. This is pretty slow if the number of menu items is too large as you predicted @spacedmonkey. I'll try to figure it out tomorrow and move this PR over to the menus-endpoints repository for further discussion.

@adamziel
Copy link
Contributor Author

adamziel commented May 13, 2020

The situation

The approach I initially took was to reuse the existing WP_REST_Menu_Items_Controller code. It works for deleting and updating existing menu items even when the number of menu items is pretty large (500+). Creating new menu items on the other hand starts timing out when more than ~100 new menu items are submitted in one batch. I profiled the execution and at first was pretty optimistic:

Zrzut ekranu 2020-05-13 o 16 55 28

But even after I pacified wp_set_object_terms and wp_unique_post_slug, wp_update_nav_menu_item is just too hungry for resources when creating a new menu item:

Zrzut ekranu 2020-05-13 o 16 56 09

Not good! It's pretty clear that we cannot use wp_update_nav_menu_item in it's current form for batch processing if we want to support large batches of inserts.

What do we do?

I can see the following options:

1. Disregard wp_update_nav_menu_item and use multi insert

This would be the exact opposite of wp_update_nav_menu_item and trade off extensibility for speed. We would "manually" build a few queries like INSERT INTO table(c1,c2,...) VALUES (v11,v12,...), (v21,v22,), ... and use them to create new menu items, taxonomies, and anything else that's required.

Advantages:

  • Extremely fast, we could likely perform the entire operation under a second.
  • Less opportunity for things to go wrong - I think we could get away with less than 10 SQL queries (compared to 16383 from the call chart above).

Disadvantages:

  • Almost no hooks would be executed which means this feature would likely be incompatible with plenty of existing plugins.
  • Inconsistent with the rest of core.

2. Ignore the speed issue for now

We could just accept this approach doesn't work well for large amount of new menu items and still proceed. To ensure noone loses their work, we could prompt the user to save the menu once they add 20 new items.

Advantages:

  • We could proceed to code reviews quite soon.

Disadvantages:

  • Inability to keep more than 20 new menu items without saving them.

3. Persist new menu items as they're added

I took a sneak peek at how the current navigation page gets around the issue cc @noisysocks . Turns out it only processes batch updates and deletes. All the inserts are happening via XHR whenever you add something to the menu. Navigation id is not assigned until you save the entire navigation. It's not perfect as you're left with unassigned menu items if you navigate away, but if it works like that ATM then we wouldn't make anything worse.

I prototyped this solution and it seems like it does the trick. I pushed a rudimentary POC to this PR.

Advantages:

  • Relatively simple on the backend.
  • We could still reuse the existing update and delete code.

Disadvantages:

  • Dangling menu items.
  • Extra frontend logic required with extra attention to synchronization (e.g. we can't create a child menu item before the parent is persisted).

Other options

Here are some things I don't think we should do:

  • Optimize wp_update_nav_menu_item - I don't think there is a way to optimize it and retain BC. It seems like it trades off speed for extensibility by design. Even if we could make it 50% faster, it's still not enough.
  • Disregard the idea of batch processing - If saving 100 menu items is slow in batch, it's going to be even slower if we do 100 roundtrips.

Let's discuss!

What do y'all think? Personally, I like options 2 and 3. I'd love to hear your opinions!

Also, @spacedmonkey I'm going to keep this PR in the Gutenberg repo for now as it's unclear what approach should we take. I'd still like to get the discussion going in the feature plugin repo some time soon.

cc @draganescu @tellthemachines

@spacedmonkey
Copy link
Member

spacedmonkey commented May 13, 2020

The current solution doesn't take into acount sites using object caching. Okay, you can batch the inserts menu items in MySQL, how do you invalidate values stored an object cache? There is no wp_cache_set_multi or wp_cache_get_multi at time of writing. So updates to cache will have to done one at a time and there is no way to roll this back.

What happens if transactions are not enabled on mysql? Does this just fail? There would need to be a solution for users without transactions, which would end up being sending one request per menu item, what you are trying to avoid in the first place.

Also not firing filters and actions will on menu items, will likely cause breakables which is not okay.

I like the idea behind this PR but I think the idea as it stands is a none starter and more time should not be invested in it.

@adamziel
Copy link
Contributor Author

adamziel commented May 13, 2020

The current solution doesn't take into acount sites using object caching. Okay, you can batch the inserts menu items in MySQL, how do you invalidate values stored an object cache? There is no wp_cache_set_multi or wp_cache_get_multi at time of writing. So updates to cache will have to done one at a time and there is no way to roll this back.

With multi-insert approach, the only way to invalidate the cache would be to loop over all the items after the query succeeds. I prefer the other two solutions to this one.

What happens if transactions are not enabled on mysql? Does this just fail?

Those users who are able to use transactions would get a much better reliability. If transactions are not available, we could just perform all the queries without wrapping them in a transaction and it would still be more bulletproof than sending a single XHR per menu item. Sure, it could still fail after 50% of SQL queries were completed, but the probability of this happening would be much lower than in case of multiple XHRs. For example, batch save wouldn't be affected by network issues between the browser and the server.

There would need to be a solution for users without transactions, which would end up being sending one request per menu item, what you are trying to avoid in the first place.

As I mentioned above, I don't think we'd have to send one request per item. Out of curiosity - how do you disable transactions in MySQL? Do you mean the storage engine for the tables would be MyISAM?

Also not firing filters and actions will on menu items, will likely cause breakables which is not okay.

Agreed - I mentioned this exact problem when considering solution 1. With solutions 2 and 3 it wouldn't be an issue - we would still fire all the filters and actions as they are now. The only unknown is what to do on rollback, but since this is an experimental endpoint then maybe we could just add an action like "menu_save_failed". Even if the rollback would be non trivial to handle, using a batch save still has benefits even without transactions.

I like the idea behind this PR but I think the idea as it stands is a none starter and more time should not be invested in it.

I hear you! I'm still pretty optimistic and think it wouldn't be that hard to make it work - let's keep the discussion going!

@noisysocks
Copy link
Member

Thanks for the thorough write-up @adamziel!

I think (1) is a no-go as we need to support existing hooks used by plugins and that (2) seems cumbersome from a UX perspective.

I agree that we should not avoid batch processing altogether as forcing the frontend to manage all this complexity and make dozens of round-trips isn't practical.

(3) is my preference. The disadvantages you listed aren't so hard to work around: we could add code to Core which cleans up dangling menu items, and we could have the saveMenuItem() action wait for isResolving( parentId ) to be false to ensure that parent menu items are saved before child menu items.

Another option not mentioned is to remove the Save Navigation button altogether and have menu items created, updated and deleted as soon as the user creates, updates or deletes a Navigation Link block. I don't like this though as it means the user doesn't have a chance to review their changes before publishing them. It also makes implementing undo/redo really hard.

@tellthemachines
Copy link
Contributor

With regards to 3:

Extra frontend logic required with extra attention to synchronization (e.g. we can't create a child menu item before the parent is persisted).

We're already doing this, because in order to save a child item we need to know the id of its parent, so I don't think it should count as a disadvantage 😄

@adamziel adamziel force-pushed the add/navigation-atomic-save branch from 437928b to f3476b1 Compare May 14, 2020 10:11
@adamziel adamziel added [Block] Navigation Affects the Navigation Block [Feature] List View Menu item in the top toolbar to select blocks from a list of links. labels May 14, 2020
@adamziel adamziel marked this pull request as ready for review May 14, 2020 12:51
@adamziel adamziel force-pushed the add/navigation-atomic-save branch from e437c4d to e29b067 Compare May 15, 2020 13:42
@adamziel
Copy link
Contributor Author

As @talldan mentioned - I see these changes as a v1 concept to keep the experimental page going, not something that's supposed to be a clean public API. I added a few unit tests and will add more on Monday.

@TimothyBJacobs
Copy link
Member

My feeling is that we should iterate on this while it's experimental, try to demonstrate that it works as an approach in terms of the end user experience, and then look at approaches to stabilizing and formalizing the endpoint once we can demonstrate this is a good approach.

I think the primary concern from myself and @spacedmonkey is whether the approach is the right one. I believe Jonny is concerned that this won't scale well and is of limited utility if we can't implement rolling back in a safe way.

My concern is that I think this better lives at an infrastructure level, and shouldn't be tied to specifically to the menus controller itself.

@draganescu
Copy link
Contributor

this better lives at an infrastructure level, and shouldn't be tied to specifically to the menus controller itself.

+1 to that

@noisysocks noisysocks added [Feature] Navigation Screen REST API Interaction Related to REST API [Type] Enhancement A suggestion for improvement. and removed [Block] Navigation Affects the Navigation Block [Feature] List View Menu item in the top toolbar to select blocks from a list of links. labels May 20, 2020
@adamziel
Copy link
Contributor Author

I'm closing this PR for now - handling this at the infrastructure level makes sense and it does not seem overly complicated. If it turns out to be, we could always revisit.

I'm pondering updating the saving logic to use the existing customizer endpoint for now just to keep the experiment going - it's would be a pretty trivial change and would keep the experiment moving forward. Once the core update lands, switching to the batch endpoint would be trivial as well.

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

Labels

REST API Interaction Related to REST API [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants