Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support batch requests in data layer #26024

Closed
wants to merge 5 commits into from

Conversation

adamziel
Copy link
Contributor

@adamziel adamziel commented Oct 12, 2020

🚧 UNDER CONSTRUCTION 🚧

Data layer support for #25096

The idea is simple: apiFetch pools related requests, sends them all as one batch, and then resolves the promises using the response of the collective batch request.

To demonstrate the idea, I just added batchAs: ``${ kind }-${ name }`` to actions.js. In reality, this should come from the caller to distinguish between related and unrelated requests:

  • Saving the site in the site editor should batch all template-parts updates as it ideally would be an atomic operation.
  • Saving one reusable block and then saving another should not be batched as failing to handle one shouldn't cause the other to fail.

Also, template-parts controller doesn't support batch requests at the moment so applying this PR will result in errors until it does. This PR is more of a launchpad for discussion than mergeable code.

@adamziel adamziel added the [Package] Core data /packages/core-data label Oct 12, 2020
@adamziel adamziel requested review from mmtr and nerrad as code owners October 12, 2020 12:24
@adamziel adamziel self-assigned this Oct 12, 2020
@github-actions
Copy link

Size Change: +306 B (0%)

Total Size: 1.19 MB

Filename Size Change
build/api-fetch/index.js 3.64 kB +288 B (7%) 🔍
build/core-data/index.js 12.1 kB +17 B (0%)
build/list-reusable-blocks/index.js 3.02 kB -1 B
build/viewport/index.js 1.75 kB +2 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.14 kB 0 B
build/annotations/index.js 3.52 kB 0 B
build/autop/index.js 2.72 kB 0 B
build/blob/index.js 668 B 0 B
build/block-directory/index.js 8.55 kB 0 B
build/block-directory/style-rtl.css 943 B 0 B
build/block-directory/style.css 942 B 0 B
build/block-editor/index.js 130 kB 0 B
build/block-editor/style-rtl.css 10.9 kB 0 B
build/block-editor/style.css 10.9 kB 0 B
build/block-library/editor-rtl.css 8.65 kB 0 B
build/block-library/editor.css 8.65 kB 0 B
build/block-library/index.js 145 kB 0 B
build/block-library/style-rtl.css 7.67 kB 0 B
build/block-library/style.css 7.66 kB 0 B
build/block-library/theme-rtl.css 741 B 0 B
build/block-library/theme.css 741 B 0 B
build/block-serialization-default-parser/index.js 1.78 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/blocks/index.js 47.6 kB 0 B
build/components/index.js 169 kB 0 B
build/components/style-rtl.css 15.5 kB 0 B
build/components/style.css 15.5 kB 0 B
build/compose/index.js 9.43 kB 0 B
build/data-controls/index.js 685 B 0 B
build/data/index.js 8.6 kB 0 B
build/date/index.js 31.9 kB 0 B
build/deprecated/index.js 772 B 0 B
build/dom-ready/index.js 568 B 0 B
build/dom/index.js 4.42 kB 0 B
build/edit-navigation/index.js 10.6 kB 0 B
build/edit-navigation/style-rtl.css 868 B 0 B
build/edit-navigation/style.css 871 B 0 B
build/edit-post/index.js 306 kB 0 B
build/edit-post/style-rtl.css 6.29 kB 0 B
build/edit-post/style.css 6.28 kB 0 B
build/edit-site/index.js 21.3 kB 0 B
build/edit-site/style-rtl.css 3.8 kB 0 B
build/edit-site/style.css 3.81 kB 0 B
build/edit-widgets/index.js 21.2 kB 0 B
build/edit-widgets/style-rtl.css 3.03 kB 0 B
build/edit-widgets/style.css 3.03 kB 0 B
build/editor/editor-styles-rtl.css 480 B 0 B
build/editor/editor-styles.css 482 B 0 B
build/editor/index.js 45.5 kB 0 B
build/editor/style-rtl.css 3.85 kB 0 B
build/editor/style.css 3.84 kB 0 B
build/element/index.js 4.45 kB 0 B
build/escape-html/index.js 734 B 0 B
build/format-library/index.js 7.49 kB 0 B
build/format-library/style-rtl.css 547 B 0 B
build/format-library/style.css 548 B 0 B
build/hooks/index.js 1.74 kB 0 B
build/html-entities/index.js 621 B 0 B
build/i18n/index.js 3.54 kB 0 B
build/is-shallow-equal/index.js 710 B 0 B
build/keyboard-shortcuts/index.js 2.39 kB 0 B
build/keycodes/index.js 1.85 kB 0 B
build/list-reusable-blocks/style-rtl.css 476 B 0 B
build/list-reusable-blocks/style.css 476 B 0 B
build/media-utils/index.js 5.13 kB 0 B
build/notices/index.js 1.69 kB 0 B
build/nux/index.js 3.27 kB 0 B
build/nux/style-rtl.css 671 B 0 B
build/nux/style.css 668 B 0 B
build/plugins/index.js 2.44 kB 0 B
build/primitives/index.js 1.34 kB 0 B
build/priority-queue/index.js 790 B 0 B
build/redux-routine/index.js 2.85 kB 0 B
build/rich-text/index.js 13 kB 0 B
build/server-side-render/index.js 2.6 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/token-list/index.js 1.24 kB 0 B
build/url/index.js 4.06 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

@TimothyBJacobs
Copy link
Member

How should we handle when the batch response returns a top-level error? Ie not one where each request has a separate error. This wouldn't be sourced from the batch controller itself, but could be generated by other code.

For me it seems like the actual batching shouldn't happen at the api-fetch layer, but in the data layer. That way we could also have a specific action to save entities in a batch and explicitly commit the batch.

Right now the state of that is just in a global variable, but I think it might be able to actually live in the store so we could inspect the state of the batching specifically.

I took a stab at that here: master...TimothyBJacobs:try/data-batch-requests It is working for me against /wp/v2/posts after enabling allow_batch in the base WP_REST_Posts_Controller. The global dispatch call needs looking at. My whole approach might also be wrong.

@ntsekouras
Copy link
Contributor

Hey 👋 ! I took a look at #25096 (@TimothyBJacobs ) and I wanted to know a bit more about the reasoning of this. If I understand correctly, you created an endpoint to validate the requests and if the input is okay, then you have to make another request to commit the changes (ex update something)?

If that's the case, shouldn't the validation be in the respective endpoint (ex update something)? Also can you point me to where such validation happens? Does it call prepare_item_for_database in WP_REST_Menu_Items_Controller?

I comment in this PR because I'd like to understand how is connected to Navigation and what are the other use cases for this, both data layer and endpoint). Thanks!

@TimothyBJacobs
Copy link
Member

Hey 👋 ! I took a look at #25096 (@TimothyBJacobs ) and I wanted to know a bit more about the reasoning of this. If I understand correctly, you created an endpoint to validate the requests and if the input is okay, then you have to make another request to commit the changes (ex update something)?

No, it is implemented on the server side as one request to the batch endpoint. If require-all-validate is passed, then each request is validated in the batch controller, and if each request passes validation, it performs the actual dispatching.

@adamziel
Copy link
Contributor Author

adamziel commented Oct 13, 2020

How should we handle when the batch response returns a top-level error? Ie not one where each request has a separate error. This wouldn't be sourced from the batch controller itself, but could be generated by other code.

@TimothyBJacobs I think it's fair to assume the entire operation failed and so the global error could be used as a failed response for each partial request.

Right now the state of that is just in a global variable, but I think it might be able to actually live in the store so we could inspect the state of the batching specifically. For me it seems like the actual batching shouldn't happen at the api-fetch layer, but in the data layer.

This is an interesting idea. A clear advantage is that it would prevent mixing batches from multiple registries. A disadvantage is that we'd be batching requests as they are before running all the middlewares. I wonder if there is some way to get the best of both worlds - I like the overall concept of keeping it in the state.

(Side note, I noticed that in your branch the list of batches is a global variable same as in this PR)

That way we could also have a specific action to save entities in a batch and explicitly commit the batch.

I don't quite agree here. While I agree that some things are easier to reason about when done explicitly, I am also a fan of sensible defaults. For example having to list if ( batchId ) { addToBatch(); } else { apiFetch(); } every time we want to support batching seems pretty verbose to me, I fear it would repetitive really quickly. As for explicit committing - I agree it should be possible, I also think most use cases could be covered by some smart auto-commit strategy (maybe 50ms time window?). In my opinion saveEditedEntityRecordsAsBatch would ideally be just saveEditedEntityRecords with one extra option (batch: true?)

I took a stab at that here: master...TimothyBJacobs:try/data-batch-requests It is working for me against /wp/v2/posts after enabling allow_batch in the base WP_REST_Posts_Controller. The global dispatch call needs looking at. My whole approach might also be wrong.

Nice! Thank you for taking it for a spin! I have some ideas how to connect both approaches - I'll think about it some more and hopefully propose a third PR tomorrow.

@TimothyBJacobs
Copy link
Member

A disadvantage is that we'd be batching requests as they are before running all the middlewares

I think this doesn't effect us for the most part. Looking at https://github.com/WordPress/gutenberg/tree/master/packages/api-fetch/src/middlewares, the only one I think we need to account for is the namespace middleware. The rest are supposed to operate on the global request or apply to GET requests which batching doesn't support.

(Side note, I noticed that in your branch the list of batches is a global variable same as in this PR)

Yeah they are right now, but since it lives in the core-data layer it should be possible to move those to store, right? Do actions triggered by a control get picked up by the reducer?

For example having to list if ( batchId ) { addToBatch(); } else { apiFetch(); } every time we want to support batching seems pretty verbose to me, I fear it would repetitive really quickly.

That's true, but I think there are only two spots where this would happen in core-data. In saveEntityRecord and deleteEntityRecord, so we might not reach the level where it is annoying I think.

If we are still worried, we could add it as a parameter to an apiFetch control that would support trigger ADD_TO_BATCH if passed a batchId.

I also think most use cases could be covered by some smart auto-commit strategy (maybe 50ms time window?). In my opinion saveEditedEntityRecordsAsBatch would ideally be just saveEditedEntityRecords with one extra option (batch: true?)

I think I'm probably not entirely understanding what the core-data side of your PR would look like. With an auto-commit strategy, would callers be tracking their dirty records on their own and then calling saveEditedEntityRecord() for each record with a batch flag?

Or are you saying they would call saveEditedEntityRecords() or saveEditedEntityRecordsAsBatch() with the signature for specifying what entities to save? If so, the committing part is transparent to the API caller as the saveEditedEntityRecords() call triggers a commitBatch.

If not, then how does the API caller get informed about the status of an entire batch?

@adamziel
Copy link
Contributor Author

I got caught up in #25859 today. I think I have an idea how to fit it all together, I'll propose a PR tomorrow to show you what I mean @TimothyBJacobs.

@adamziel
Copy link
Contributor Author

@TimothyBJacobs I explored state-based processing in #26164 (WIP). I thought that batch processing is a pretty generic concept so there could be a generic state machinery for handling that. Supporting batch API requests would then only be a matter of integrating that with either the entity store or API_FETCH control. What do you think about this direction? also cc @youknowriad

@TimothyBJacobs
Copy link
Member

That looks like a great approach to me!

I'm still curious about this bit to get an idea of how this will integrate with core-data.

I think I'm probably not entirely understanding what the core-data side of your PR would look like. With an auto-commit strategy, would callers be tracking their dirty records on their own and then calling saveEditedEntityRecord() for each record with a batch flag?

Or are you saying they would call saveEditedEntityRecords() or saveEditedEntityRecordsAsBatch() with the signature for specifying what entities to save? If so, the committing part is transparent to the API caller as the saveEditedEntityRecords() call triggers a commitBatch.

@youknowriad
Copy link
Contributor

I'm mainly interested in the impact this will have on callers, a batchAs property on the "query" object doesn't sound so great.

@TimothyBJacobs
Copy link
Member

I'm mainly interested in the impact this will have on callers, a batchAs property on the "query" object doesn't sound so great.

The REST API won't support GET batch requests in v1, so something to keep in mind, but I don't think we necessarily need to solve in the first go at this.

@adamziel
Copy link
Contributor Author

I'm mainly interested in the impact this will have on callers, a batchAs property on the "query" object doesn't sound so great.

Just to be strict, that's a request or options object. But yes, I think we can do better than that. We may not be able to work out the details of a proper core data integration before 9.2 so as @TimothyBJacobs said, this is not necessarily a problem for the first go. For now let's focus on making it work just in the widgets editor.

Also, I'm closing this PR in favor of #26164 which is based on redux store.

@adamziel adamziel closed this Oct 16, 2020
@aristath aristath deleted the update/data-layer-batch-requests branch November 10, 2020 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Core data /packages/core-data
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants