scuffle_batching/
lib.rs

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