scuffle_batching/
lib.rs

1//! A crate designed to batch multiple requests into a single request.
2#![cfg_attr(feature = "docs", doc = "\n\nSee the [changelog][changelog] for a full release history.")]
3#![cfg_attr(feature = "docs", doc = "## Feature flags")]
4#![cfg_attr(feature = "docs", doc = document_features::document_features!())]
5//! ## Why do we need this?
6//!
7//! Often when we are building applications we need to load multiple items from
8//! a database or some other external resource. It is often expensive to load
9//! each item individually, and this is typically why most drivers have some
10//! form of multi-item loading or executing. This crate provides an improved
11//! version of this functionality by combining multiple calls from different
12//! scopes into a single batched request.
13//!
14//! ## Tradeoffs
15//!
16//! Because we are buffering requests for a short period of time we do see
17//! higher latencies when there are not many requests. This is because the
18//! overhead from just processing the requests is lower then the time we spend
19//! buffering.
20//!
21//! However, this is often negated when we have a large number of requests as we
22//! see on average lower latencies due to more efficient use of resources.
23//! Latency is also more consistent as we are doing fewer requests to the
24//! external resource.
25//!
26//! ## Usage
27//!
28//! Here is an example of how to use the `DataLoader` interface to batch
29//! multiple reads from a database.
30//!
31//! ```rust
32//! # use std::collections::{HashSet, HashMap};
33//! # use scuffle_batching::{DataLoaderFetcher, DataLoader, dataloader::DataLoaderBuilder};
34//! # tokio_test::block_on(async {
35//! # #[derive(Clone, Hash, Eq, PartialEq)]
36//! # struct User {
37//! #     pub id: i64,
38//! # }
39//! # struct SomeDatabase;
40//! # impl SomeDatabase {
41//! #    fn fetch(&self, query: &str) -> Fetch {
42//! #       Fetch
43//! #    }
44//! # }
45//! # struct Fetch;
46//! # impl Fetch {
47//! #     async fn bind(&self, user_ids: HashSet<i64>) -> Result<Vec<User>, &'static str> {
48//! #         Ok(vec![])
49//! #     }
50//! # }
51//! # let database = SomeDatabase;
52//! struct MyUserLoader(SomeDatabase);
53//!
54//! impl DataLoaderFetcher for MyUserLoader {
55//!     type Key = i64;
56//!     type Value = User;
57//!
58//!     async fn load(&self, keys: HashSet<Self::Key>) -> Option<HashMap<Self::Key, Self::Value>> {
59//!         let users = self.0.fetch("SELECT * FROM users WHERE id IN ($1)").bind(keys).await.map_err(|e| {
60//!             eprintln!("Failed to fetch users: {}", e);
61//!         }).ok()?;
62//!
63//!         Some(users.into_iter().map(|user| (user.id, user)).collect())
64//!     }
65//! }
66//!
67//! let loader = DataLoaderBuilder::new().build(MyUserLoader(database));
68//!
69//! // Will only make a single request to the database and load both users
70//! // You can also use `loader.load_many` if you have more then one item to load.
71//! let (user1, user2): (Result<_, _>, Result<_, _>) = tokio::join!(loader.load(1), loader.load(2));
72//! # });
73//! ```
74//!
75//! Another use case might be to batch multiple writes to a database.
76//!
77//! ```rust
78//! # use std::collections::HashSet;
79//! # use scuffle_batching::{DataLoaderFetcher, BatchExecutor, Batcher, batch::{BatchResponse, BatcherBuilder}, DataLoader};
80//! # tokio_test::block_on(async move {
81//! # #[derive(Clone, Hash, Eq, PartialEq)]
82//! # struct User {
83//! #     pub id: i64,
84//! # }
85//! # struct SomeDatabase;
86//! # impl SomeDatabase {
87//! #    fn update(&self, query: &str) -> Update {
88//! #       Update
89//! #    }
90//! # }
91//! # struct Update;
92//! # impl Update {
93//! #     async fn bind(&self, users: Vec<User>) -> Result<Vec<User>, &'static str> {
94//! #         Ok(vec![])
95//! #     }
96//! # }
97//! # let database = SomeDatabase;
98//! struct MyUserUpdater(SomeDatabase);
99//!
100//! impl BatchExecutor for MyUserUpdater {
101//!     type Request = User;
102//!     type Response = bool;
103//!
104//!     async fn execute(&self, requests: Vec<(Self::Request, BatchResponse<Self::Response>)>) {
105//!         let (users, responses): (Vec<Self::Request>, Vec<BatchResponse<Self::Response>>) = requests.into_iter().unzip();
106//!
107//!         // You would need to build the query somehow, this is just an example
108//!         if let Err(e) = self.0.update("INSERT INTO users (id, name) VALUES ($1, $2), ($3, $4)").bind(users).await {
109//!             eprintln!("Failed to insert users: {}", e);
110//!
111//!             for response in responses {
112//!                 // Reply back saying we failed
113//!                 response.send(false);
114//!             }
115//!
116//!             return;
117//!         }
118//!
119//!         // Reply back to the client that we successfully inserted the users
120//!         for response in responses {
121//!             response.send(true);
122//!         }
123//!     }
124//! }
125//!
126//! let batcher = BatcherBuilder::new().build(MyUserUpdater(database));
127//! # let user1 = User { id: 1 };
128//! # let user2 = User { id: 2 };
129//! // Will only make a single request to the database and insert both users
130//! // You can also use `batcher.execute_many` if you have more then one item to insert.
131//! let (success1, success2) = tokio::join!(batcher.execute(user1), batcher.execute(user2));
132//!
133//! if success1.is_some_and(|s| !s) {
134//!     eprintln!("Failed to insert user 1");
135//! }
136//!
137//! if success2.is_some_and(|s| !s) {
138//!     eprintln!("Failed to insert user 2");
139//! }
140//! # });
141//! ```
142//!
143//! ## License
144//!
145//! This project is licensed under the MIT or Apache-2.0 license.
146//! You can choose between one of them if you use this work.
147//!
148//! `SPDX-License-Identifier: MIT OR Apache-2.0`
149#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
150#![cfg_attr(docsrs, feature(doc_auto_cfg))]
151#![deny(missing_docs)]
152#![deny(unsafe_code)]
153#![deny(unreachable_pub)]
154
155pub mod batch;
156pub mod dataloader;
157
158pub use batch::{BatchExecutor, Batcher};
159pub use dataloader::{DataLoader, DataLoaderFetcher};
160
161/// Changelogs generated by [scuffle_changelog]
162#[cfg(feature = "docs")]
163#[scuffle_changelog::changelog]
164pub mod changelog {}