Batch requests

← Batch requests

Ashterix

docs: rewrite JsonRpcBundle docs, add SDK docs, move them under docs/api

en/docs/api/json-rpc-bundle/functionality/batch.md new file +238 āˆ’0

1 + # Batch requests
2 +
3 + :::info
4 + **Batch API requests** are an approach that lets you combine several requests into one. Instead of sending many separate requests to the server, the client can send a single request containing several operations. The server receives this request, processes all the operations and returns one combined response that includes the results of all requests.
5 + :::
6 +
7 + ## The problem
8 +
9 + Imagine we are developing a backend API for an online store, and we already have a ProductService.getInfo method for the Product entity. The frontend task is to implement the logic of adding products to the cart and refreshing product information on demand (for example, when the cart is opened, we need to update information about the products the user has added: availability, prices, descriptions, etc.).
10 +
11 + ## The scenario 100% of developers follow
12 +
13 + A backend task is created to build a separate API method that returns a collection of objects for an array of identifiers. This takes the working time of a backend developer, a QA engineer and a frontend developer, because the new method has to be covered by unit tests as well as additional regression test scenarios. All of this has to be done even though we already have a method that can return a single product by its id.
14 +
15 + ## The scenario nobody follows
16 +
17 + Send a separate API request for each product to get up-to-date information. If there are 10 products in the cart, that means 10 HTTP requests. This puts a significant load on the network and increases the user's waiting time, which makes it an unacceptable option.
18 +
19 + ## The alternative using batch requests
20 +
21 + If we can send and process batch requests, we can greatly simplify the implementation of the business logic. We can use a batch request to combine all these requests into one. We send one request to the API that contains all the product identifiers, and the server returns information about all the products in a single response. As a result, we get:
22 +
23 + 1. **Universality**: Using batch requests avoids creating specialized methods for every case. This simplifies the API and reduces the amount of code required.
24 + 2. **Convenience for the client**: The client can decide which requests to combine, which makes the API more flexible and convenient to use.
25 + 3. **Less backend complexity**: Instead of writing and maintaining specialized methods for different scenarios, you can use the universal batch request approach, which simplifies the backend architecture.
26 +
27 + :::success
28 + This approach makes more efficient use of resources and improves performance by reducing the load on the network and servers, and it also improves the user experience.
29 + :::
30 +
31 + :::warning subtitle="Caveat"
32 + In high-load systems such a scenario will not be efficient, but there are other optimization options that we will cover in other articles.
33 + :::
34 +
35 + **Example**
36 +
37 + Let's look at the request from the example above
38 +
39 + :::::columns
40 + ::::col
41 + **Request**
42 +
43 + ```json
44 + [
45 + {
46 + "id":"example_1",
47 + "method":"ProductService.getInfo",
48 + "params":{
49 + "productId": 345234
50 + }
51 + },
52 + {
53 + "id":"example_2",
54 + "method":"ProductService.getInfo",
55 + "params":{
56 + "productId": 994234
57 + }
58 + }
59 + ]
60 + ```
61 +
62 + :::warning subtitle="Important!!!"
63 + Always specify unique request ids in batch requests, because responses may be returned in a different order.
64 + :::
65 + ::::
66 +
67 + :::col
68 + **Response**
69 +
70 + ```json
71 + [
72 + {
73 + "id":"example_2",
74 + "result": {
75 + "id": 994234,
76 + "name": "product 2",
77 + "price": 11.99,
78 + "balance": 28
79 + }
80 + },
81 + {
82 + "id":"example_1",
83 + "result": {
84 + "id": 345234,
85 + "name": "product 1",
86 + "price": 99.99,
87 + "balance": 14
88 + }
89 + }
90 + ]
91 + ```
92 + :::
93 + :::::
94 +
95 + ### Advantages of batch requests
96 +
97 + 1. **Simpler backend:** Instead of implementing specific logic on the backend every time, you can move it to the level of building client requests.
98 + 2. **Fewer network requests**: Instead of sending several HTTP requests, which can load the network and the server, only one request is used. This significantly reduces the load on the network and server resources.
99 + 3. **Response time**: The RPC Server returns the result of all requests, regardless of their number, in the time needed to process the longest request in the batch, because all requests are processed [in parallel](/docs/api/json-rpc-bundle/functionality/batch/).
100 + 4. **Lower latency**: Executing several requests over one connection can reduce the overall latency, since less time is spent on establishing and closing connections.
101 + 5. **Optimized client and server resource usage**: Since fewer requests are processed at the same time, the load on the server decreases, allowing it to handle more requests. The client also uses fewer resources, which can be critical for mobile and constrained devices.
102 + 6. **Better transaction control**: When several operations need to be executed atomically (all or none), a batch request makes this easier, since all operations are executed within a single request.
103 +
104 + ### Getting started
105 +
106 + Batch requests are available out of the box. How the items are executed is defined by the `batch` block in the [configuration](/docs/api/json-rpc-bundle/config) — see the "Execution modes" section below.
107 +
108 + ### Request order
109 +
110 + The order of items in a batch does not matter. An error in one item does not affect the processing of the others: the response contains an `error` object for it and a `result` for the rest.
111 +
112 + ### Dependent requests
113 +
114 + :::info
115 + The batch request mechanism lets you create dependent requests. This is a request in which one or more parameters depend on the response of another request in the list.
116 + :::
117 +
118 + Imagine that the frontend needs to get a user's email address by their identifier and then send them a message at that address. We already have the UserService.getInfo and `Messenger.sendEmail` methods. In the classic scenario this would be either two requests, or a separate backend method that sends an email by user id would have to be created.
119 +
120 + Using the `JsonRpcBundle` library by UFO-Tech, you can create batch requests in which one request can depend on another and receives its response in order to run. In the context of the example, one request gets the user information, and the second request uses the obtained email to send the message.
121 +
122 + ::::columns
123 + :::col
124 + **Request**
125 +
126 + ```json
127 + [
128 + {
129 + "jsonrpc": "2.0",
130 + "id":"example_1",
131 + "method":"UserService.getInfo",
132 + "params":{
133 + "userId": 141
134 + }
135 + },
136 + {
137 + "jsonrpc": "2.0",
138 + "id":"example_2",
139 + "method":"Messenger.sendEmail",
140 + "params":{
141 + "email": "@FROM:example_1(email)",
142 + "subject": "Welcome!",
143 + "message": "Thank you for joining our service!"
144 + }
145 + }
146 + ]
147 + ```
148 + :::
149 +
150 + :::col
151 + **Response**
152 +
153 + ```json
154 + [
155 + {
156 + "id":"example_1",
157 + "result": {
158 + "id": 141,
159 + "name": "John",
160 + "email": "john.dou@example.com",
161 + "status": 1
162 + }
163 + },
164 + {
165 + "id":"example_2",
166 + "result": "Email for 'john.dou@example.com' sent"
167 + }
168 + ]
169 + ```
170 + :::
171 + ::::
172 +
173 + Dependency format: `@FROM:<request id>(<result field>)`. Dependencies work in both execution modes.
174 +
175 + In this example:
176 +
177 + 1. The first request gets information about the user with id 141, including their email.
178 + 2. The second request sends an email to the obtained address, using data from the first request.
179 +
180 + :::info subtitle="The order of requests does not matter!"
181 + The RPC Server detects request dependencies itself and queues them in the right order.
182 + :::
183 +
184 + :::warning
185 + Dependent requests are executed sequentially, so the response to the batch request is returned after the dependent requests have completed.
186 + :::
187 +
188 + ### Advantages of dependent requests:
189 +
190 + 1. **Convenience and efficiency**: Dependent requests let you perform complex operations with minimal effort while ensuring the correct order of request execution.
191 + 2. **Fewer network requests**: All requests are combined into one, which reduces the network load.
192 + 3. **Flexibility**: You can build complex request scenarios without additional configuration or backend changes.
193 +
194 + ## Execution modes
195 +
196 + ```yaml
197 + # config/packages/ufo_json_rpc.yaml
198 + ufo_json_rpc:
199 + batch:
200 + enabled: true # false: batch requests are rejected with a single -32600 "Batch requests are disabled" error
201 + process: true # true: each item in a child process; false: one by one in the current process
202 + process_count: 2 # process mode: how many child processes run at the same time (1–10)
203 + ```
204 +
205 + | Mode | How it works | When to choose |
206 + | --- | --- | --- |
207 + | process (`process: true`, default) | Each item is executed in a separate `ufo:rpc:process` process ([Symfony Process](https://symfony.com/doc/current/components/process.html)); no more than `process_count` processes run at the same time, the rest wait | Long independent methods that need isolation and parallelism |
208 + | inline (`process: false`) | Items are executed sequentially in the current process | Short methods: roughly 250 times faster, because no PHP process is started for each item |
209 +
210 + :::warning subtitle="Upgrading to 10.3"
211 + Since 10.3 `process_count` defaults to 2, so large batches in process mode run slower than before. Set `process_count: 10` or switch to `process: false`.
212 + :::
213 +
214 + The token is passed to child processes via the `UFO_RPC_TOKEN` environment variable and the request via stdin, so they do not show up in the process list. Child processes keep the API [group](/docs/api/json-rpc-bundle/functionality/groups) and [version](/docs/api/json-rpc-bundle/functionality/versions) of the route.
215 +
216 + ### Item timeout
217 +
218 + In process mode, the response of an item is awaited for a limited time: 10 seconds by default. If there is no result, an error is returned for that item. For long methods, the timeout is increased with the special `$rpc.timeout` parameter:
219 +
220 + ```json
221 + [
222 + {"jsonrpc": "2.0", "id": "example_1", "method": "ExampleApi.fastMethod", "params": {"someParam": "someValue"}},
223 + {"jsonrpc": "2.0", "id": "example_2", "method": "ExampleApi.longMethod", "params": {"someParam": "someValue", "$rpc": {"timeout": 30}}}
224 + ]
225 + ```
226 +
227 + ## Notifications in a batch
228 +
229 + An item without an `id` is a [notification](/docs/api/json-rpc-bundle/functionality/notifications). According to the JSON-RPC 2.0 specification, there is no response to it:
230 +
231 + * a notification is executed according to the batch mode, but the response contains only the other items;
232 + * a batch of notifications only returns `204` with no body;
233 + * a notification whose `@FROM` dependency failed is not executed.
234 +
235 + ## Batch errors
236 +
237 + * An empty batch `[]` — a single `-32600` error (`Can't process empty batch request`).
238 + * A batch with `batch.enabled: false` — a single `-32600` error (`Batch requests are disabled`).