From 41e6d50a7dd44da3b9a15218d7a106ec86ca9094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=AFd?= <20957603+ayewo@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:47:24 +0100 Subject: [PATCH 1/8] docs: add a guide on Batching in Tailcall --- docs/guides/batching.md | 361 +++++++++++++++++++++ static/images/meal-delivery-app.drawio.png | Bin 0 -> 43363 bytes 2 files changed, 361 insertions(+) create mode 100644 docs/guides/batching.md create mode 100644 static/images/meal-delivery-app.drawio.png diff --git a/docs/guides/batching.md b/docs/guides/batching.md new file mode 100644 index 0000000000..f477c7e548 --- /dev/null +++ b/docs/guides/batching.md @@ -0,0 +1,361 @@ +--- +title: "Batching" +--- + +Batching when configured correctly, can significantly reduce strain on high-traffic REST backends. You only need to add a handful of operators to your GraphQL schema (i.e. custom directives) for Tailcall to do most of the heavy lifting for you[^1]. + +## Scenario + +Catered lunches and healthy snacks are some of the free perks startups offer to office employees, but with the rise of remote work, many startups are leaving this tradition. + +One way to keep the tradition going would be a universal meal delivery application in Slack, available as a `/ship-meal` command. Managers, anywhere in the world, could use the command to have lunch delivered to teammates, including those that work from home. + +### Multiple Vendors +![image](--- +title: "Batching" +--- + +Batching when configured correctly, can significantly reduce strain on high-traffic REST backends. You only need to add a handful of operators to your GraphQL schema (i.e. custom directives) for Tailcall to do most of the heavy lifting for you[^1]. + +## Scenario + +Catered lunches and healthy snacks are some of the free perks startups offer to office employees, but with the rise of remote work, many startups are leaving this tradition. + +One way to keep the tradition going would be a universal meal delivery application in Slack, available as a `/ship-meal` command. Managers, anywhere in the world, could use the command to have lunch delivered to teammates, including those that work from home. + +### Multiple Vendors +![image](static/images/meal-delivery-app.drawio.png) + +### Constraints + +The nature of such a service could cause traffic spikes for every upstream vendor needed to fulfil each individual order, so some care has to be taken when designing the meal app's API. +For instance, if thousands of managers across the world use the command at the same time (just before lunch break) to place team orders, the sudden traffic spike will spread to upstream vendors, leading to delayed or failed orders. + +*Batching* is a one technique that can be used to avoid overwhelming upstream servers with too many simulatenous requests. Batching combines multiple operations in a bulk operation that is sent in a single request. + +Tailcall supports batching via two [operators](https://tailcall.run/docs/operators/): `@upstream` and `@http`. +Before we go over this Tailcall feature, we'll briefly review the most common implementations of batching in REST APIs. + +--- + +## Batching in REST APIs +### High Correspondence Between REST and CRUD + +In backends that adhere to the REST architectural style, the HTTP methods `POST`, `GET`, `PUT`, and `DELETE` roughly correspond to *Create*, *Read*, *Update*, and *Delete* operations respectively in the CRUD paradigm as can be seen in the table below. + +| HTTP method | CRUD paradigm | +|-------------|---------------| +| `POST` | *Create* | +| `GET` | *Read* | +| `PUT` | *Update* | +| `DELETE` | *Delete* | + +This one-to-one correspondence works because CRUD and REST often deal with a single entity or resource as the case may be. +```bash + POST /v1/employees (Create an employee entity) + GET /v1/employees/:id (Read an employee entity) + PUT /v1/employees/:id (Update an employee entity) +DELETE /v1/employees/:id (Delete an employee entity) + GET /v1/employees (Read multiple employee entities) +``` + +### Low Correspondence Between Batching and CRUD + +The one-to-one correspondence doesn't carry over when batching is added to a REST API because batching can either involve: +* performing the same operation on different entities of the same type (e.g. *Update* the team's order so meals are shipped to employees with the following ids `1`, `4` & `7`) or; +* grouping together different operations in one request (e.g. *Create* `Jain` as a new employee, *Update* an employee's meal preferences and *Delete* meals above a certain price from the menu). + +### Real-world Examples of Batching + +The table below condenses the most common URL styles used to implement batching in the real-world. + +| Operation | HTTP method | URL style | Parameters | Content type | Example | +|-----------|-----------|-------------|-------------|-------------|---------| +| *Read* | | | | | | +| | `GET` | 1. `/users?id=1&id=4&id=7` | URL query params | - | [github.com](https://github.com/search?q=user%3Adefunkt+user%3Aubuntu+user%3Amojombo&type=users) | +| | `GET` | 2. `/users/1,4,7` | URL path params | - | [ipstack.com](https://ipstack.com/documentation#bulk) | +| | `POST` | 3. `/users/` | Request body | `application/json` | [ipinfo.io](https://ipinfo.io/developers/advanced-usage#batching-requests) | +| *CRUD* | | | | | | +| | `POST` | 4. `/users?batch` | Request body | `application/json` | [facebook.com](https://developers.facebook.com/docs/graph-api/batch-requests) | +| | `POST` | 5. `/batch/submitJob` (async) | Request body | `application/json` | [arcgis.com](https://developers.arcgis.com/rest/services-reference/enterprise/batch-geocode.htm) | +| | `POST` | 6. `/batch/` | Request body | `multipart/mixed` | [google.com](https://cloud.google.com/storage/docs/batch) | + +### Batching: Sync or Async + +REST APIs that support batching can either follow a synchronous or asynchronous style depending on the underlying operation. + +The sync style is the most common and is often used for operations that are short-lived i.e. operations that can be completed quickly so that the server can return an immediate response (`200 OK`). In fact, out of the 6 different URL styles shown in the table above, only URL style #5 is asynchronous, the rest are synchronous. + +The async style is used in situations where an operation can take a considerable amount of time to complete. The server will process the request asynchronously but will return an immediate response (`202 Accepted`)[^202-Accepted] instead of letting the client wait. + +### Receiving an Async Response: Pull or Push + +Within the async request style, there are two ways of retrieving a response from the server: pull or push. + +In the pull model: the client periodically polls the server to check that the operation has completed successfully, or failed. + +In the push model: when the operation is complete, the server pushes the results over an existing subscription with the client such as a web socket (for browser-based clients) or a web hook (for server-based clients).[^delivery] + +### Open Data Protocol + +URL style #6 in the table above, uses a `Content-type` of `multipart/mixed` which makes it the most flexible way of implementing batching in REST APIs. It allows clients to submit arbitrary operations (multiple *Create*, *Read*, *Update*, and *Delete* operations, each with its own `Content-type`) in a single request, though most services enforce a limit[^batch-size-limit] in the range of 10-1000 called the batch size. The batch size is the number of sub-requests that can be included in a single request to an endpoint that supports batching. + +This style of batching has been made into a standard by Microsoft under the [Open Data Protocol](https://en.wikipedia.org/wiki/Open_Data_Protocol) as the [OData batch processing system](https://www.odata.org/documentation/odata-version-3-0/batch-processing/). Sharepoint Online and Office 365 REST APIs are examples of Microsoft services that support [batching with their REST API](https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/make-batch-requests-with-the-rest-apis) using the OData standard. + +Some Google services have adopted the OData syntax, though the semantics differ. Examples include Google [Cloud Storage](https://cloud.google.com/storage/docs/batch), which supports up to 100 requests in a single batch request and Google [Workspace Admin API](https://developers.google.com/admin-sdk/directory/v1/guides/batch), which supports up to 1000 requests in a single batch request. + + + +--- + +## Batching in Tailcall + +Tailcall supports batching `GET` requests in REST APIs that follow the design in URL style #1 in the table above. The batch size is configurable and can be set via `@upstream(... batch.maxSize)`. + +Let's now return to our meal delivery app to illustrate how it works. + +### Meal Prep and Delivery +Before meals are prepped, the meal delivery app will first check how many meals it will need to make for each company and the location of the employees where each meal will be delivered. Since employees may sometimes switch between working at the office, at a co-working space or from home, the app tries to estimate each employee's current location by geolocation of their IP address. + +```graphql showLineNumbers +schema + @server(port: 8000, graphiql: true) +# highlight-start + @upstream(baseURL: "https://geoip-batch.fly.dev", httpCache: true, batch: {delay: 1, maxSize: 100}) { +# highlight-end + query: Query +} + +type Query { + users: [User]! @http(path: "/users") +} + +type User { + id: Int! + username: String! + email: String! + phone: String + ip: String! +# highlight-start + country: Country! @http(path: "/batch", query: [{key: "query", value: "{{value.ip}}"}], groupBy: ["query"]) +# highlight-end +} + +type Country { + query: String + country: String + regionName: String + city: String + lat: Float + lon: Float +} +``` +A lot of geolocation services support batch requests to save on network round-trips. The sample `graphql` shows how batching could be used to lookup the location of multiple employees using only one batch request with the batch size set to `100` sub-requests. + +When you run the following GraphQL query: +```graphql +{ + users { + id + username + email + phone + ip + country { + query + country + regionName + city + lat + lon + } + } +} +``` + +It will produce the following output in Tailcall: +```bash +2024-01-22T13:57:33Z INFO tailcall::cli::tc] N + 1: 0 +[2024-01-22T13:57:33Z INFO tailcall::http] 🚀 Tailcall launched at [127.0.0.1:8000] over HTTP/1.1 +[2024-01-22T13:57:33Z INFO tailcall::http] 🌍 Playground: http://127.0.0.1:8000 +[2024-01-22T13:58:04Z INFO tailcall::http::client] GET https://geoip-batch.fly.dev/users HTTP/1.1 +[2024-01-22T13:58:05Z INFO tailcall::http::client] GET https://geoip-batch.fly.dev/batch?query=100.159.51.104&query=103.72.86.183&query=116.92.198.102&query=117.29.86.254&query=137.235.164.173&query=141.14.53.176&query=163.245.232.27&query=174.238.43.126&query=197.37.13.163&query=205.226.160.3&query=25.207.107.146&query=29.82.54.30&query=43.20.78.113&query=48.30.193.203&query=49.201.206.36&query=51.102.180.216&query=53.240.20.181&query=59.43.194.22&query=71.57.235.192&query=73.15.179.178&query=74.80.53.208&query=75.75.234.243&query=78.170.185.120&query=78.43.74.226&query=82.170.69.15&query=87.213.156.73&query=90.202.216.39&query=91.200.56.127&query=93.246.47.59&query=97.11.116.84 HTTP/1.1 +``` + +The `/users` endpoint returns a total of 30 users. As you can see in the output, Tailcall constructed a batch request using the IP addresses of all 30 users rather than make 30 individual requests to the geolocation service. + + +[^1]: To take full advantage of batching, the REST backends being proxied with Tailcall must themselves have support for batching i.e. they must support the ability to combine multiple individual requests into a single request. + +[^delivery]: https://news.ycombinator.com/item?id=28392042 + +[^202-Accepted]: https://www.mscharhag.com/api-design/bulk-and-batch-operations + +[^batch-size-limit]: https://www.codementor.io/blog/batch-endpoints-6olbjay1hd#other-considerations-for-batch-processing) + +### Constraints + +The nature of such a service could cause traffic spikes for every upstream vendor needed to fulfil each individual order, so some care has to be taken when designing the meal app's API. +For instance, if thousands of managers across the world use the command at the same time (just before lunch break) to place team orders, the sudden traffic spike will spread to upstream vendors, leading to delayed or failed orders. + +*Batching* is a one technique that can be used to avoid overwhelming upstream servers with too many simulatenous requests. Batching combines multiple operations in a bulk operation that is sent in a single request. + +Tailcall supports batching via two [operators](https://tailcall.run/docs/operators/): `@upstream` and `@http`. +Before we go over this Tailcall feature, we'll briefly review the most common implementations of batching in REST APIs. + +--- + +## Batching in REST APIs +### High Correspondence Between REST and CRUD + +In backends that adhere to the REST architectural style, the HTTP methods `POST`, `GET`, `PUT`, and `DELETE` roughly correspond to *Create*, *Read*, *Update*, and *Delete* operations respectively in the CRUD paradigm as can be seen in the table below. + +| HTTP method | CRUD paradigm | +|-------------|---------------| +| `POST` | *Create* | +| `GET` | *Read* | +| `PUT` | *Update* | +| `DELETE` | *Delete* | + +This one-to-one correspondence works because CRUD and REST often deal with a single entity or resource as the case may be. +```bash + POST /v1/employees (Create an employee entity) + GET /v1/employees/:id (Read an employee entity) + PUT /v1/employees/:id (Update an employee entity) +DELETE /v1/employees/:id (Delete an employee entity) + GET /v1/employees (Read multiple employee entities) +``` + +### Low Correspondence Between Batching and CRUD + +The one-to-one correspondence doesn't carry over when batching is added to a REST API because batching can either involve: +* performing the same operation on different entities of the same type (e.g. *Update* the team's order so meals are shipped to employees with the following ids `1`, `4` & `7`) or; +* grouping together different operations in one request (e.g. *Create* `Jain` as a new employee, *Update* an employee's meal preferences and *Delete* meals above a certain price from the menu). + +### Real-world Examples of Batching + +The table below condenses the most common URL styles used to implement batching in the real-world. + +| Operation | HTTP method | URL style | Parameters | Content type | Example | +|-----------|-----------|-------------|-------------|-------------|---------| +| *Read* | | | | | | +| | `GET` | 1. `/users?id=1&id=4&id=7` | URL query params | - | [github.com](https://github.com/search?q=user%3Adefunkt+user%3Aubuntu+user%3Amojombo&type=users) | +| | `GET` | 2. `/users/1,4,7` | URL path params | - | [ipstack.com](https://ipstack.com/documentation#bulk) | +| | `POST` | 3. `/users/` | Request body | `application/json` | [ipinfo.io](https://ipinfo.io/developers/advanced-usage#batching-requests) | +| *CRUD* | | | | | | +| | `POST` | 4. `/users?batch` | Request body | `application/json` | [facebook.com](https://developers.facebook.com/docs/graph-api/batch-requests) | +| | `POST` | 5. `/batch/submitJob` (async) | Request body | `application/json` | [arcgis.com](https://developers.arcgis.com/rest/services-reference/enterprise/batch-geocode.htm) | +| | `POST` | 6. `/batch/` | Request body | `multipart/mixed` | [google.com](https://cloud.google.com/storage/docs/batch) | + +### Batching: Sync or Async + +REST APIs that support batching can either follow a synchronous or asynchronous style depending on the underlying operation. + +The sync style is the most common and is often used for operations that are short-lived i.e. operations that can be completed quickly so that the server can return an immediate response (`200 OK`). In fact, out of the 6 different URL styles shown in the table above, only URL style #5 is asynchronous, the rest are synchronous. + +The async style is used in situations where an operation can take a considerable amount of time to complete. The server will process the request asynchronously but will return an immediate response (`202 Accepted`)[^202-Accepted] instead of letting the client wait. + +### Receiving an Async Response: Pull or Push + +Within the async request style, there are two ways of retrieving a response from the server: pull or push. + +In the pull model: the client periodically polls the server to check that the operation has completed successfully, or failed. + +In the push model: when the operation is complete, the server pushes the results over an existing subscription with the client such as a web socket (for browser-based clients) or a web hook (for server-based clients).[^delivery] + +### Open Data Protocol + +URL style #6 in the table above, uses a `Content-type` of `multipart/mixed` which makes it the most flexible way of implementing batching in REST APIs. It allows clients to submit arbitrary operations (multiple *Create*, *Read*, *Update*, and *Delete* operations, each with its own `Content-type`) in a single request, though most services enforce a limit[^batch-size-limit] in the range of 10-1000 called the batch size. The batch size is the number of sub-requests that can be included in a single request to an endpoint that supports batching. + +This style of batching has been made into a standard by Microsoft under the [Open Data Protocol](https://en.wikipedia.org/wiki/Open_Data_Protocol) as the [OData batch processing system](https://www.odata.org/documentation/odata-version-3-0/batch-processing/). Sharepoint Online and Office 365 REST APIs are examples of Microsoft services that support [batching with their REST API](https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/make-batch-requests-with-the-rest-apis) using the OData standard. + +Some Google services have adopted the OData syntax, though the semantics differ. Examples include Google [Cloud Storage](https://cloud.google.com/storage/docs/batch), which supports up to 100 requests in a single batch request and Google [Workspace Admin API](https://developers.google.com/admin-sdk/directory/v1/guides/batch), which supports up to 1000 requests in a single batch request. + + + +--- + +## Batching in Tailcall + +Tailcall supports batching `GET` requests in REST APIs that follow the design in URL style #1 in the table above. The batch size is configurable and can be set via `@upstream(... batch.maxSize)`. + +Let's now return to our meal delivery app to illustrate how it works. + +### Meal Prep and Delivery +Before meals are prepped, the meal delivery app will first check how many meals it will need to make for each company and the location of the employees where each meal will be delivered. Since employees may sometimes switch between working at the office, at a co-working space or from home, the app tries to estimate each employee's current location by geolocation of their IP address. + +```graphql showLineNumbers +schema + @server(port: 8000, graphiql: true) +# highlight-start + @upstream(baseURL: "https://geoip-batch.fly.dev", httpCache: true, batch: {delay: 1, maxSize: 100}) { +# highlight-end + query: Query +} + +type Query { + users: [User]! @http(path: "/users") +} + +type User { + id: Int! + username: String! + email: String! + phone: String + ip: String! +# highlight-start + country: Country! @http(path: "/batch", query: [{key: "query", value: "{{value.ip}}"}], groupBy: ["query"]) +# highlight-end +} + +type Country { + query: String + country: String + regionName: String + city: String + lat: Float + lon: Float +} +``` +A lot of geolocation services support batch requests to save on network round-trips. The sample `graphql` shows how batching could be used to lookup the location of multiple employees using only one batch request with the batch size set to `100` sub-requests. + +When you run the following GraphQL query: +```graphql +{ + users { + id + username + email + phone + ip + country { + query + country + regionName + city + lat + lon + } + } +} +``` + +It will produce the following output in Tailcall: +```bash +2024-01-22T13:57:33Z INFO tailcall::cli::tc] N + 1: 0 +[2024-01-22T13:57:33Z INFO tailcall::http] 🚀 Tailcall launched at [127.0.0.1:8000] over HTTP/1.1 +[2024-01-22T13:57:33Z INFO tailcall::http] 🌍 Playground: http://127.0.0.1:8000 +[2024-01-22T13:58:04Z INFO tailcall::http::client] GET https://geoip-batch.fly.dev/users HTTP/1.1 +[2024-01-22T13:58:05Z INFO tailcall::http::client] GET https://geoip-batch.fly.dev/batch?query=100.159.51.104&query=103.72.86.183&query=116.92.198.102&query=117.29.86.254&query=137.235.164.173&query=141.14.53.176&query=163.245.232.27&query=174.238.43.126&query=197.37.13.163&query=205.226.160.3&query=25.207.107.146&query=29.82.54.30&query=43.20.78.113&query=48.30.193.203&query=49.201.206.36&query=51.102.180.216&query=53.240.20.181&query=59.43.194.22&query=71.57.235.192&query=73.15.179.178&query=74.80.53.208&query=75.75.234.243&query=78.170.185.120&query=78.43.74.226&query=82.170.69.15&query=87.213.156.73&query=90.202.216.39&query=91.200.56.127&query=93.246.47.59&query=97.11.116.84 HTTP/1.1 +``` + +The `/users` endpoint returns a total of 30 users. As you can see in the output, Tailcall constructed a batch request using the IP addresses of all 30 users rather than make 30 individual requests to the geolocation service. + + +[^1]: To take full advantage of batching, the REST backends being proxied with Tailcall must themselves have support for batching i.e. they must support the ability to combine multiple individual requests into a single request. + +[^delivery]: https://news.ycombinator.com/item?id=28392042 + +[^202-Accepted]: https://www.mscharhag.com/api-design/bulk-and-batch-operations + +[^batch-size-limit]: https://www.codementor.io/blog/batch-endpoints-6olbjay1hd#other-considerations-for-batch-processing \ No newline at end of file diff --git a/static/images/meal-delivery-app.drawio.png b/static/images/meal-delivery-app.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..786fb68262ff1ed1080f9ed845b0124b341965dd GIT binary patch literal 43363 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 R%17XjtfZ1extLMl{{TTgbeRAE literal 0 HcmV?d00001 From 974fbcc0cfb2cb7b0131546e84411a24b733f8df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=AFd?= <20957603+ayewo@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:51:39 +0100 Subject: [PATCH 2/8] docs: add a guide on Batching in Tailcall. --- docs/guides/batching.md | 180 ---------------------------------------- 1 file changed, 180 deletions(-) diff --git a/docs/guides/batching.md b/docs/guides/batching.md index f477c7e548..e9ac01eead 100644 --- a/docs/guides/batching.md +++ b/docs/guides/batching.md @@ -10,19 +10,6 @@ Catered lunches and healthy snacks are some of the free perks startups offer to One way to keep the tradition going would be a universal meal delivery application in Slack, available as a `/ship-meal` command. Managers, anywhere in the world, could use the command to have lunch delivered to teammates, including those that work from home. -### Multiple Vendors -![image](--- -title: "Batching" ---- - -Batching when configured correctly, can significantly reduce strain on high-traffic REST backends. You only need to add a handful of operators to your GraphQL schema (i.e. custom directives) for Tailcall to do most of the heavy lifting for you[^1]. - -## Scenario - -Catered lunches and healthy snacks are some of the free perks startups offer to office employees, but with the rise of remote work, many startups are leaving this tradition. - -One way to keep the tradition going would be a universal meal delivery application in Slack, available as a `/ship-meal` command. Managers, anywhere in the world, could use the command to have lunch delivered to teammates, including those that work from home. - ### Multiple Vendors ![image](static/images/meal-delivery-app.drawio.png) @@ -106,173 +93,6 @@ Some Google services have adopted the OData syntax, though the semantics differ. ---- - -## Batching in Tailcall - -Tailcall supports batching `GET` requests in REST APIs that follow the design in URL style #1 in the table above. The batch size is configurable and can be set via `@upstream(... batch.maxSize)`. - -Let's now return to our meal delivery app to illustrate how it works. - -### Meal Prep and Delivery -Before meals are prepped, the meal delivery app will first check how many meals it will need to make for each company and the location of the employees where each meal will be delivered. Since employees may sometimes switch between working at the office, at a co-working space or from home, the app tries to estimate each employee's current location by geolocation of their IP address. - -```graphql showLineNumbers -schema - @server(port: 8000, graphiql: true) -# highlight-start - @upstream(baseURL: "https://geoip-batch.fly.dev", httpCache: true, batch: {delay: 1, maxSize: 100}) { -# highlight-end - query: Query -} - -type Query { - users: [User]! @http(path: "/users") -} - -type User { - id: Int! - username: String! - email: String! - phone: String - ip: String! -# highlight-start - country: Country! @http(path: "/batch", query: [{key: "query", value: "{{value.ip}}"}], groupBy: ["query"]) -# highlight-end -} - -type Country { - query: String - country: String - regionName: String - city: String - lat: Float - lon: Float -} -``` -A lot of geolocation services support batch requests to save on network round-trips. The sample `graphql` shows how batching could be used to lookup the location of multiple employees using only one batch request with the batch size set to `100` sub-requests. - -When you run the following GraphQL query: -```graphql -{ - users { - id - username - email - phone - ip - country { - query - country - regionName - city - lat - lon - } - } -} -``` - -It will produce the following output in Tailcall: -```bash -2024-01-22T13:57:33Z INFO tailcall::cli::tc] N + 1: 0 -[2024-01-22T13:57:33Z INFO tailcall::http] 🚀 Tailcall launched at [127.0.0.1:8000] over HTTP/1.1 -[2024-01-22T13:57:33Z INFO tailcall::http] 🌍 Playground: http://127.0.0.1:8000 -[2024-01-22T13:58:04Z INFO tailcall::http::client] GET https://geoip-batch.fly.dev/users HTTP/1.1 -[2024-01-22T13:58:05Z INFO tailcall::http::client] GET https://geoip-batch.fly.dev/batch?query=100.159.51.104&query=103.72.86.183&query=116.92.198.102&query=117.29.86.254&query=137.235.164.173&query=141.14.53.176&query=163.245.232.27&query=174.238.43.126&query=197.37.13.163&query=205.226.160.3&query=25.207.107.146&query=29.82.54.30&query=43.20.78.113&query=48.30.193.203&query=49.201.206.36&query=51.102.180.216&query=53.240.20.181&query=59.43.194.22&query=71.57.235.192&query=73.15.179.178&query=74.80.53.208&query=75.75.234.243&query=78.170.185.120&query=78.43.74.226&query=82.170.69.15&query=87.213.156.73&query=90.202.216.39&query=91.200.56.127&query=93.246.47.59&query=97.11.116.84 HTTP/1.1 -``` - -The `/users` endpoint returns a total of 30 users. As you can see in the output, Tailcall constructed a batch request using the IP addresses of all 30 users rather than make 30 individual requests to the geolocation service. - - -[^1]: To take full advantage of batching, the REST backends being proxied with Tailcall must themselves have support for batching i.e. they must support the ability to combine multiple individual requests into a single request. - -[^delivery]: https://news.ycombinator.com/item?id=28392042 - -[^202-Accepted]: https://www.mscharhag.com/api-design/bulk-and-batch-operations - -[^batch-size-limit]: https://www.codementor.io/blog/batch-endpoints-6olbjay1hd#other-considerations-for-batch-processing) - -### Constraints - -The nature of such a service could cause traffic spikes for every upstream vendor needed to fulfil each individual order, so some care has to be taken when designing the meal app's API. -For instance, if thousands of managers across the world use the command at the same time (just before lunch break) to place team orders, the sudden traffic spike will spread to upstream vendors, leading to delayed or failed orders. - -*Batching* is a one technique that can be used to avoid overwhelming upstream servers with too many simulatenous requests. Batching combines multiple operations in a bulk operation that is sent in a single request. - -Tailcall supports batching via two [operators](https://tailcall.run/docs/operators/): `@upstream` and `@http`. -Before we go over this Tailcall feature, we'll briefly review the most common implementations of batching in REST APIs. - ---- - -## Batching in REST APIs -### High Correspondence Between REST and CRUD - -In backends that adhere to the REST architectural style, the HTTP methods `POST`, `GET`, `PUT`, and `DELETE` roughly correspond to *Create*, *Read*, *Update*, and *Delete* operations respectively in the CRUD paradigm as can be seen in the table below. - -| HTTP method | CRUD paradigm | -|-------------|---------------| -| `POST` | *Create* | -| `GET` | *Read* | -| `PUT` | *Update* | -| `DELETE` | *Delete* | - -This one-to-one correspondence works because CRUD and REST often deal with a single entity or resource as the case may be. -```bash - POST /v1/employees (Create an employee entity) - GET /v1/employees/:id (Read an employee entity) - PUT /v1/employees/:id (Update an employee entity) -DELETE /v1/employees/:id (Delete an employee entity) - GET /v1/employees (Read multiple employee entities) -``` - -### Low Correspondence Between Batching and CRUD - -The one-to-one correspondence doesn't carry over when batching is added to a REST API because batching can either involve: -* performing the same operation on different entities of the same type (e.g. *Update* the team's order so meals are shipped to employees with the following ids `1`, `4` & `7`) or; -* grouping together different operations in one request (e.g. *Create* `Jain` as a new employee, *Update* an employee's meal preferences and *Delete* meals above a certain price from the menu). - -### Real-world Examples of Batching - -The table below condenses the most common URL styles used to implement batching in the real-world. - -| Operation | HTTP method | URL style | Parameters | Content type | Example | -|-----------|-----------|-------------|-------------|-------------|---------| -| *Read* | | | | | | -| | `GET` | 1. `/users?id=1&id=4&id=7` | URL query params | - | [github.com](https://github.com/search?q=user%3Adefunkt+user%3Aubuntu+user%3Amojombo&type=users) | -| | `GET` | 2. `/users/1,4,7` | URL path params | - | [ipstack.com](https://ipstack.com/documentation#bulk) | -| | `POST` | 3. `/users/` | Request body | `application/json` | [ipinfo.io](https://ipinfo.io/developers/advanced-usage#batching-requests) | -| *CRUD* | | | | | | -| | `POST` | 4. `/users?batch` | Request body | `application/json` | [facebook.com](https://developers.facebook.com/docs/graph-api/batch-requests) | -| | `POST` | 5. `/batch/submitJob` (async) | Request body | `application/json` | [arcgis.com](https://developers.arcgis.com/rest/services-reference/enterprise/batch-geocode.htm) | -| | `POST` | 6. `/batch/` | Request body | `multipart/mixed` | [google.com](https://cloud.google.com/storage/docs/batch) | - -### Batching: Sync or Async - -REST APIs that support batching can either follow a synchronous or asynchronous style depending on the underlying operation. - -The sync style is the most common and is often used for operations that are short-lived i.e. operations that can be completed quickly so that the server can return an immediate response (`200 OK`). In fact, out of the 6 different URL styles shown in the table above, only URL style #5 is asynchronous, the rest are synchronous. - -The async style is used in situations where an operation can take a considerable amount of time to complete. The server will process the request asynchronously but will return an immediate response (`202 Accepted`)[^202-Accepted] instead of letting the client wait. - -### Receiving an Async Response: Pull or Push - -Within the async request style, there are two ways of retrieving a response from the server: pull or push. - -In the pull model: the client periodically polls the server to check that the operation has completed successfully, or failed. - -In the push model: when the operation is complete, the server pushes the results over an existing subscription with the client such as a web socket (for browser-based clients) or a web hook (for server-based clients).[^delivery] - -### Open Data Protocol - -URL style #6 in the table above, uses a `Content-type` of `multipart/mixed` which makes it the most flexible way of implementing batching in REST APIs. It allows clients to submit arbitrary operations (multiple *Create*, *Read*, *Update*, and *Delete* operations, each with its own `Content-type`) in a single request, though most services enforce a limit[^batch-size-limit] in the range of 10-1000 called the batch size. The batch size is the number of sub-requests that can be included in a single request to an endpoint that supports batching. - -This style of batching has been made into a standard by Microsoft under the [Open Data Protocol](https://en.wikipedia.org/wiki/Open_Data_Protocol) as the [OData batch processing system](https://www.odata.org/documentation/odata-version-3-0/batch-processing/). Sharepoint Online and Office 365 REST APIs are examples of Microsoft services that support [batching with their REST API](https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/make-batch-requests-with-the-rest-apis) using the OData standard. - -Some Google services have adopted the OData syntax, though the semantics differ. Examples include Google [Cloud Storage](https://cloud.google.com/storage/docs/batch), which supports up to 100 requests in a single batch request and Google [Workspace Admin API](https://developers.google.com/admin-sdk/directory/v1/guides/batch), which supports up to 1000 requests in a single batch request. - - - --- ## Batching in Tailcall From 51ec2157606b4730a9a27b9a0f9dfbcd5e5f2d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=AFd?= <20957603+ayewo@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:53:51 +0100 Subject: [PATCH 3/8] docs: fix typo. --- docs/guides/batching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/batching.md b/docs/guides/batching.md index e9ac01eead..5a629e0889 100644 --- a/docs/guides/batching.md +++ b/docs/guides/batching.md @@ -18,7 +18,7 @@ One way to keep the tradition going would be a universal meal delivery applicati The nature of such a service could cause traffic spikes for every upstream vendor needed to fulfil each individual order, so some care has to be taken when designing the meal app's API. For instance, if thousands of managers across the world use the command at the same time (just before lunch break) to place team orders, the sudden traffic spike will spread to upstream vendors, leading to delayed or failed orders. -*Batching* is a one technique that can be used to avoid overwhelming upstream servers with too many simulatenous requests. Batching combines multiple operations in a bulk operation that is sent in a single request. +*Batching* is a one technique that can be used to avoid overwhelming upstream servers with too many simultaneous requests. Batching combines multiple operations in a bulk operation that is sent in a single request. Tailcall supports batching via two [operators](https://tailcall.run/docs/operators/): `@upstream` and `@http`. Before we go over this Tailcall feature, we'll briefly review the most common implementations of batching in REST APIs. From 50c1c6f929640b45db308ebf73bec303c05d1a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=AFd?= <20957603+ayewo@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:21:31 +0100 Subject: [PATCH 4/8] fix: prettier formatting --- docs/guides/batching.md | 78 +++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/docs/guides/batching.md b/docs/guides/batching.md index 5a629e0889..6f2ea9c8f8 100644 --- a/docs/guides/batching.md +++ b/docs/guides/batching.md @@ -11,6 +11,7 @@ Catered lunches and healthy snacks are some of the free perks startups offer to One way to keep the tradition going would be a universal meal delivery application in Slack, available as a `/ship-meal` command. Managers, anywhere in the world, could use the command to have lunch delivered to teammates, including those that work from home. ### Multiple Vendors + ![image](static/images/meal-delivery-app.drawio.png) ### Constraints @@ -18,26 +19,28 @@ One way to keep the tradition going would be a universal meal delivery applicati The nature of such a service could cause traffic spikes for every upstream vendor needed to fulfil each individual order, so some care has to be taken when designing the meal app's API. For instance, if thousands of managers across the world use the command at the same time (just before lunch break) to place team orders, the sudden traffic spike will spread to upstream vendors, leading to delayed or failed orders. -*Batching* is a one technique that can be used to avoid overwhelming upstream servers with too many simultaneous requests. Batching combines multiple operations in a bulk operation that is sent in a single request. +_Batching_ is a one technique that can be used to avoid overwhelming upstream servers with too many simultaneous requests. Batching combines multiple operations in a bulk operation that is sent in a single request. -Tailcall supports batching via two [operators](https://tailcall.run/docs/operators/): `@upstream` and `@http`. +Tailcall supports batching via two [operators](https://tailcall.run/docs/operators/): `@upstream` and `@http`. Before we go over this Tailcall feature, we'll briefly review the most common implementations of batching in REST APIs. --- ## Batching in REST APIs + ### High Correspondence Between REST and CRUD -In backends that adhere to the REST architectural style, the HTTP methods `POST`, `GET`, `PUT`, and `DELETE` roughly correspond to *Create*, *Read*, *Update*, and *Delete* operations respectively in the CRUD paradigm as can be seen in the table below. +In backends that adhere to the REST architectural style, the HTTP methods `POST`, `GET`, `PUT`, and `DELETE` roughly correspond to _Create_, _Read_, _Update_, and _Delete_ operations respectively in the CRUD paradigm as can be seen in the table below. | HTTP method | CRUD paradigm | -|-------------|---------------| -| `POST` | *Create* | -| `GET` | *Read* | -| `PUT` | *Update* | -| `DELETE` | *Delete* | +| ----------- | ------------- | +| `POST` | _Create_ | +| `GET` | _Read_ | +| `PUT` | _Update_ | +| `DELETE` | _Delete_ | + +This one-to-one correspondence works because CRUD and REST often deal with a single entity or resource as the case may be. -This one-to-one correspondence works because CRUD and REST often deal with a single entity or resource as the case may be. ```bash POST /v1/employees (Create an employee entity) GET /v1/employees/:id (Read an employee entity) @@ -49,27 +52,28 @@ DELETE /v1/employees/:id (Delete an employee entity) ### Low Correspondence Between Batching and CRUD The one-to-one correspondence doesn't carry over when batching is added to a REST API because batching can either involve: -* performing the same operation on different entities of the same type (e.g. *Update* the team's order so meals are shipped to employees with the following ids `1`, `4` & `7`) or; -* grouping together different operations in one request (e.g. *Create* `Jain` as a new employee, *Update* an employee's meal preferences and *Delete* meals above a certain price from the menu). + +- performing the same operation on different entities of the same type (e.g. _Update_ the team's order so meals are shipped to employees with the following ids `1`, `4` & `7`) or; +- grouping together different operations in one request (e.g. _Create_ `Jain` as a new employee, _Update_ an employee's meal preferences and _Delete_ meals above a certain price from the menu). ### Real-world Examples of Batching The table below condenses the most common URL styles used to implement batching in the real-world. -| Operation | HTTP method | URL style | Parameters | Content type | Example | -|-----------|-----------|-------------|-------------|-------------|---------| -| *Read* | | | | | | -| | `GET` | 1. `/users?id=1&id=4&id=7` | URL query params | - | [github.com](https://github.com/search?q=user%3Adefunkt+user%3Aubuntu+user%3Amojombo&type=users) | -| | `GET` | 2. `/users/1,4,7` | URL path params | - | [ipstack.com](https://ipstack.com/documentation#bulk) | -| | `POST` | 3. `/users/` | Request body | `application/json` | [ipinfo.io](https://ipinfo.io/developers/advanced-usage#batching-requests) | -| *CRUD* | | | | | | -| | `POST` | 4. `/users?batch` | Request body | `application/json` | [facebook.com](https://developers.facebook.com/docs/graph-api/batch-requests) | -| | `POST` | 5. `/batch/submitJob` (async) | Request body | `application/json` | [arcgis.com](https://developers.arcgis.com/rest/services-reference/enterprise/batch-geocode.htm) | -| | `POST` | 6. `/batch/` | Request body | `multipart/mixed` | [google.com](https://cloud.google.com/storage/docs/batch) | +| Operation | HTTP method | URL style | Parameters | Content type | Example | +| --------- | ----------- | ----------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------ | +| _Read_ | | | | | | +| | `GET` | 1. `/users?id=1&id=4&id=7` | URL query params | - | [github.com](https://github.com/search?q=user%3Adefunkt+user%3Aubuntu+user%3Amojombo&type=users) | +| | `GET` | 2. `/users/1,4,7` | URL path params | - | [ipstack.com](https://ipstack.com/documentation#bulk) | +| | `POST` | 3. `/users/` | Request body | `application/json` | [ipinfo.io](https://ipinfo.io/developers/advanced-usage#batching-requests) | +| _CRUD_ | | | | | | +| | `POST` | 4. `/users?batch` | Request body | `application/json` | [facebook.com](https://developers.facebook.com/docs/graph-api/batch-requests) | +| | `POST` | 5. `/batch/submitJob` (async) | Request body | `application/json` | [arcgis.com](https://developers.arcgis.com/rest/services-reference/enterprise/batch-geocode.htm) | +| | `POST` | 6. `/batch/` | Request body | `multipart/mixed` | [google.com](https://cloud.google.com/storage/docs/batch) | ### Batching: Sync or Async -REST APIs that support batching can either follow a synchronous or asynchronous style depending on the underlying operation. +REST APIs that support batching can either follow a synchronous or asynchronous style depending on the underlying operation. The sync style is the most common and is often used for operations that are short-lived i.e. operations that can be completed quickly so that the server can return an immediate response (`200 OK`). In fact, out of the 6 different URL styles shown in the table above, only URL style #5 is asynchronous, the rest are synchronous. @@ -79,20 +83,18 @@ The async style is used in situations where an operation can take a considerable Within the async request style, there are two ways of retrieving a response from the server: pull or push. -In the pull model: the client periodically polls the server to check that the operation has completed successfully, or failed. +In the pull model: the client periodically polls the server to check that the operation has completed successfully, or failed. In the push model: when the operation is complete, the server pushes the results over an existing subscription with the client such as a web socket (for browser-based clients) or a web hook (for server-based clients).[^delivery] ### Open Data Protocol -URL style #6 in the table above, uses a `Content-type` of `multipart/mixed` which makes it the most flexible way of implementing batching in REST APIs. It allows clients to submit arbitrary operations (multiple *Create*, *Read*, *Update*, and *Delete* operations, each with its own `Content-type`) in a single request, though most services enforce a limit[^batch-size-limit] in the range of 10-1000 called the batch size. The batch size is the number of sub-requests that can be included in a single request to an endpoint that supports batching. +URL style #6 in the table above, uses a `Content-type` of `multipart/mixed` which makes it the most flexible way of implementing batching in REST APIs. It allows clients to submit arbitrary operations (multiple _Create_, _Read_, _Update_, and _Delete_ operations, each with its own `Content-type`) in a single request, though most services enforce a limit[^batch-size-limit] in the range of 10-1000 called the batch size. The batch size is the number of sub-requests that can be included in a single request to an endpoint that supports batching. This style of batching has been made into a standard by Microsoft under the [Open Data Protocol](https://en.wikipedia.org/wiki/Open_Data_Protocol) as the [OData batch processing system](https://www.odata.org/documentation/odata-version-3-0/batch-processing/). Sharepoint Online and Office 365 REST APIs are examples of Microsoft services that support [batching with their REST API](https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/make-batch-requests-with-the-rest-apis) using the OData standard. Some Google services have adopted the OData syntax, though the semantics differ. Examples include Google [Cloud Storage](https://cloud.google.com/storage/docs/batch), which supports up to 100 requests in a single batch request and Google [Workspace Admin API](https://developers.google.com/admin-sdk/directory/v1/guides/batch), which supports up to 1000 requests in a single batch request. - - --- ## Batching in Tailcall @@ -102,14 +104,15 @@ Tailcall supports batching `GET` requests in REST APIs that follow the design in Let's now return to our meal delivery app to illustrate how it works. ### Meal Prep and Delivery + Before meals are prepped, the meal delivery app will first check how many meals it will need to make for each company and the location of the employees where each meal will be delivered. Since employees may sometimes switch between working at the office, at a co-working space or from home, the app tries to estimate each employee's current location by geolocation of their IP address. ```graphql showLineNumbers schema @server(port: 8000, graphiql: true) -# highlight-start + # highlight-start @upstream(baseURL: "https://geoip-batch.fly.dev", httpCache: true, batch: {delay: 1, maxSize: 100}) { -# highlight-end + # highlight-end query: Query } @@ -123,9 +126,9 @@ type User { email: String! phone: String ip: String! -# highlight-start + # highlight-start country: Country! @http(path: "/batch", query: [{key: "query", value: "{{value.ip}}"}], groupBy: ["query"]) -# highlight-end + # highlight-end } type Country { @@ -137,9 +140,11 @@ type Country { lon: Float } ``` + A lot of geolocation services support batch requests to save on network round-trips. The sample `graphql` shows how batching could be used to lookup the location of multiple employees using only one batch request with the batch size set to `100` sub-requests. -When you run the following GraphQL query: +When you run the following GraphQL query: + ```graphql { users { @@ -160,7 +165,8 @@ When you run the following GraphQL query: } ``` -It will produce the following output in Tailcall: +It will produce the following output in Tailcall: + ```bash 2024-01-22T13:57:33Z INFO tailcall::cli::tc] N + 1: 0 [2024-01-22T13:57:33Z INFO tailcall::http] 🚀 Tailcall launched at [127.0.0.1:8000] over HTTP/1.1 @@ -169,13 +175,9 @@ It will produce the following output in Tailcall: [2024-01-22T13:58:05Z INFO tailcall::http::client] GET https://geoip-batch.fly.dev/batch?query=100.159.51.104&query=103.72.86.183&query=116.92.198.102&query=117.29.86.254&query=137.235.164.173&query=141.14.53.176&query=163.245.232.27&query=174.238.43.126&query=197.37.13.163&query=205.226.160.3&query=25.207.107.146&query=29.82.54.30&query=43.20.78.113&query=48.30.193.203&query=49.201.206.36&query=51.102.180.216&query=53.240.20.181&query=59.43.194.22&query=71.57.235.192&query=73.15.179.178&query=74.80.53.208&query=75.75.234.243&query=78.170.185.120&query=78.43.74.226&query=82.170.69.15&query=87.213.156.73&query=90.202.216.39&query=91.200.56.127&query=93.246.47.59&query=97.11.116.84 HTTP/1.1 ``` -The `/users` endpoint returns a total of 30 users. As you can see in the output, Tailcall constructed a batch request using the IP addresses of all 30 users rather than make 30 individual requests to the geolocation service. - +The `/users` endpoint returns a total of 30 users. As you can see in the output, Tailcall constructed a batch request using the IP addresses of all 30 users rather than make 30 individual requests to the geolocation service. [^1]: To take full advantage of batching, the REST backends being proxied with Tailcall must themselves have support for batching i.e. they must support the ability to combine multiple individual requests into a single request. - [^delivery]: https://news.ycombinator.com/item?id=28392042 - [^202-Accepted]: https://www.mscharhag.com/api-design/bulk-and-batch-operations - -[^batch-size-limit]: https://www.codementor.io/blog/batch-endpoints-6olbjay1hd#other-considerations-for-batch-processing \ No newline at end of file +[^batch-size-limit]: https://www.codementor.io/blog/batch-endpoints-6olbjay1hd#other-considerations-for-batch-processing From efce6bf8e708badac5648c6afdf9e810e1acf7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=AFd?= <20957603+ayewo@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:53:04 +0100 Subject: [PATCH 5/8] fix: image path --- docs/guides/batching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/batching.md b/docs/guides/batching.md index 6f2ea9c8f8..63fa7d9b31 100644 --- a/docs/guides/batching.md +++ b/docs/guides/batching.md @@ -12,7 +12,7 @@ One way to keep the tradition going would be a universal meal delivery applicati ### Multiple Vendors -![image](static/images/meal-delivery-app.drawio.png) +![image](../../static/images/meal-delivery-app.drawio.png) ### Constraints From a760606807a62ba5c95d7e8b030e500503263d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=AFd?= <20957603+ayewo@users.noreply.github.com> Date: Wed, 21 Feb 2024 22:14:26 +0100 Subject: [PATCH 6/8] chore(docs): apply review suggestions - intro Co-authored-by: Kiryl Mialeshka <8974488+meskill@users.noreply.github.com> --- docs/guides/batching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/batching.md b/docs/guides/batching.md index 63fa7d9b31..db77b515e5 100644 --- a/docs/guides/batching.md +++ b/docs/guides/batching.md @@ -2,7 +2,7 @@ title: "Batching" --- -Batching when configured correctly, can significantly reduce strain on high-traffic REST backends. You only need to add a handful of operators to your GraphQL schema (i.e. custom directives) for Tailcall to do most of the heavy lifting for you[^1]. +Batching when configured correctly, can significantly reduce strain on high-traffic backends. You only need to add a handful of operators to your GraphQL schema (i.e. custom directives) for Tailcall to do most of the heavy lifting for you[^1]. ## Scenario From 29cbf6b8bbb4116f24cff9c4f336204d1bfb19aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=AFd?= <20957603+ayewo@users.noreply.github.com> Date: Sun, 25 Feb 2024 16:01:37 +0100 Subject: [PATCH 7/8] chore(docs): fixes for review feedback --- docs/guides/batching.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/guides/batching.md b/docs/guides/batching.md index db77b515e5..d92dbc2bd5 100644 --- a/docs/guides/batching.md +++ b/docs/guides/batching.md @@ -2,6 +2,8 @@ title: "Batching" --- +One of the ways developers can handle the dreaded [N+1 problem](https://tailcall.run/docs/guides/n+1/) is to use batching. + Batching when configured correctly, can significantly reduce strain on high-traffic backends. You only need to add a handful of operators to your GraphQL schema (i.e. custom directives) for Tailcall to do most of the heavy lifting for you[^1]. ## Scenario @@ -91,9 +93,6 @@ In the push model: when the operation is complete, the server pushes the results URL style #6 in the table above, uses a `Content-type` of `multipart/mixed` which makes it the most flexible way of implementing batching in REST APIs. It allows clients to submit arbitrary operations (multiple _Create_, _Read_, _Update_, and _Delete_ operations, each with its own `Content-type`) in a single request, though most services enforce a limit[^batch-size-limit] in the range of 10-1000 called the batch size. The batch size is the number of sub-requests that can be included in a single request to an endpoint that supports batching. -This style of batching has been made into a standard by Microsoft under the [Open Data Protocol](https://en.wikipedia.org/wiki/Open_Data_Protocol) as the [OData batch processing system](https://www.odata.org/documentation/odata-version-3-0/batch-processing/). Sharepoint Online and Office 365 REST APIs are examples of Microsoft services that support [batching with their REST API](https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/make-batch-requests-with-the-rest-apis) using the OData standard. - -Some Google services have adopted the OData syntax, though the semantics differ. Examples include Google [Cloud Storage](https://cloud.google.com/storage/docs/batch), which supports up to 100 requests in a single batch request and Google [Workspace Admin API](https://developers.google.com/admin-sdk/directory/v1/guides/batch), which supports up to 1000 requests in a single batch request. --- @@ -141,7 +140,15 @@ type Country { } ``` -A lot of geolocation services support batch requests to save on network round-trips. The sample `graphql` shows how batching could be used to lookup the location of multiple employees using only one batch request with the batch size set to `100` sub-requests. +A lot of geolocation services support batch requests to save on network round-trips. The sample `graphql` shows how to lookup the location of multiple employees using only one batch request. + +The Tailcall [`@upstream`](https://tailcall.run/docs/operators/upstream/) operator exposes several properties that allows developers to control various aspects of the upstream server connection, including how requests are batched. + +In our example above, I enabled HTTP caching by setting the [`httpCache`](https://tailcall.run/docs/operators/upstream/#httpcache) property to `true` since it defaults to `false`. + +I also configured [`batch`](https://tailcall.run/docs/operators/upstream/#batch) object which controls batching. I set `delay: 1` indicating a delay of 1 millisecond between each batch request (to avoid getting throttled by the upstream server) and set `maxSize: 100` indicating the Tailcall can issue up to `100` sub-requests as part of a single batch request. + + When you run the following GraphQL query: @@ -175,7 +182,10 @@ It will produce the following output in Tailcall: [2024-01-22T13:58:05Z INFO tailcall::http::client] GET https://geoip-batch.fly.dev/batch?query=100.159.51.104&query=103.72.86.183&query=116.92.198.102&query=117.29.86.254&query=137.235.164.173&query=141.14.53.176&query=163.245.232.27&query=174.238.43.126&query=197.37.13.163&query=205.226.160.3&query=25.207.107.146&query=29.82.54.30&query=43.20.78.113&query=48.30.193.203&query=49.201.206.36&query=51.102.180.216&query=53.240.20.181&query=59.43.194.22&query=71.57.235.192&query=73.15.179.178&query=74.80.53.208&query=75.75.234.243&query=78.170.185.120&query=78.43.74.226&query=82.170.69.15&query=87.213.156.73&query=90.202.216.39&query=91.200.56.127&query=93.246.47.59&query=97.11.116.84 HTTP/1.1 ``` -The `/users` endpoint returns a total of 30 users. As you can see in the output, Tailcall constructed a batch request using the IP addresses of all 30 users rather than make 30 individual requests to the geolocation service. +The `/users` endpoint returns a total of 30 users. As you can see in the output, Tailcall constructed a batch request that concatenates the IP addresses of all 30 users in one request, rather than make 30 individual requests to the geolocation service. + +Batching is an optimization technique for mitigating the [N+1 problem](https://tailcall.run/docs/guides/n+1/) as it can significantly reduce the number of network round-trips needed to fulfil a request when one or more upstream servers are involved. + [^1]: To take full advantage of batching, the REST backends being proxied with Tailcall must themselves have support for batching i.e. they must support the ability to combine multiple individual requests into a single request. [^delivery]: https://news.ycombinator.com/item?id=28392042 From bf9234424da48287d4acdd6fc6c4995ba054df03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=AFd?= <20957603+ayewo@users.noreply.github.com> Date: Sun, 25 Feb 2024 18:29:12 +0100 Subject: [PATCH 8/8] chore: fix prettier formatting --- docs/guides/batching.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/guides/batching.md b/docs/guides/batching.md index d92dbc2bd5..d8a2f4696b 100644 --- a/docs/guides/batching.md +++ b/docs/guides/batching.md @@ -93,7 +93,6 @@ In the push model: when the operation is complete, the server pushes the results URL style #6 in the table above, uses a `Content-type` of `multipart/mixed` which makes it the most flexible way of implementing batching in REST APIs. It allows clients to submit arbitrary operations (multiple _Create_, _Read_, _Update_, and _Delete_ operations, each with its own `Content-type`) in a single request, though most services enforce a limit[^batch-size-limit] in the range of 10-1000 called the batch size. The batch size is the number of sub-requests that can be included in a single request to an endpoint that supports batching. - --- ## Batching in Tailcall @@ -148,8 +147,6 @@ In our example above, I enabled HTTP caching by setting the [`httpCache`](https: I also configured [`batch`](https://tailcall.run/docs/operators/upstream/#batch) object which controls batching. I set `delay: 1` indicating a delay of 1 millisecond between each batch request (to avoid getting throttled by the upstream server) and set `maxSize: 100` indicating the Tailcall can issue up to `100` sub-requests as part of a single batch request. - - When you run the following GraphQL query: ```graphql @@ -186,7 +183,6 @@ The `/users` endpoint returns a total of 30 users. As you can see in the output, Batching is an optimization technique for mitigating the [N+1 problem](https://tailcall.run/docs/guides/n+1/) as it can significantly reduce the number of network round-trips needed to fulfil a request when one or more upstream servers are involved. - [^1]: To take full advantage of batching, the REST backends being proxied with Tailcall must themselves have support for batching i.e. they must support the ability to combine multiple individual requests into a single request. [^delivery]: https://news.ycombinator.com/item?id=28392042 [^202-Accepted]: https://www.mscharhag.com/api-design/bulk-and-batch-operations