1use crate::{
7 error::PoolError, AddedTransactionOutcome, PoolTransaction, TransactionOrigin, TransactionPool,
8};
9use futures_util::FutureExt;
10use pin_project::pin_project;
11use std::{
12 future::Future,
13 pin::Pin,
14 task::{ready, Context, Poll},
15};
16use tokio::sync::{mpsc, oneshot};
17
18#[derive(Debug)]
20pub struct BatchTxRequest<T: PoolTransaction> {
21 origin: TransactionOrigin,
23 pool_tx: T,
25 response_tx: oneshot::Sender<Result<AddedTransactionOutcome, PoolError>>,
27}
28
29impl<T> BatchTxRequest<T>
30where
31 T: PoolTransaction,
32{
33 pub const fn new(
35 origin: TransactionOrigin,
36 pool_tx: T,
37 response_tx: oneshot::Sender<Result<AddedTransactionOutcome, PoolError>>,
38 ) -> Self {
39 Self { origin, pool_tx, response_tx }
40 }
41}
42
43#[pin_project]
45#[derive(Debug)]
46pub struct BatchTxProcessor<Pool: TransactionPool> {
47 pool: Pool,
48 max_batch_size: usize,
49 buf: Vec<BatchTxRequest<Pool::Transaction>>,
50 #[pin]
51 request_rx: mpsc::UnboundedReceiver<BatchTxRequest<Pool::Transaction>>,
52}
53
54impl<Pool> BatchTxProcessor<Pool>
55where
56 Pool: TransactionPool + 'static,
57{
58 pub fn new(
60 pool: Pool,
61 max_batch_size: usize,
62 ) -> (Self, mpsc::UnboundedSender<BatchTxRequest<Pool::Transaction>>) {
63 let (request_tx, request_rx) = mpsc::unbounded_channel();
64
65 let processor = Self { pool, max_batch_size, buf: Vec::with_capacity(1), request_rx };
66
67 (processor, request_tx)
68 }
69
70 async fn process_request(pool: &Pool, req: BatchTxRequest<Pool::Transaction>) {
71 let BatchTxRequest { origin, pool_tx, response_tx } = req;
72 let pool_result = pool.add_transaction(origin, pool_tx).await;
73 let _ = response_tx.send(pool_result);
74 }
75
76 async fn process_batch(pool: &Pool, batch: Vec<BatchTxRequest<Pool::Transaction>>) {
78 if batch.len() == 1 {
79 Self::process_request(pool, batch.into_iter().next().expect("batch is not empty"))
80 .await;
81 return
82 }
83
84 let mut batch_iter = batch.iter();
86 if let Some(origin) = batch_iter.next().map(|req| req.origin) &&
87 batch_iter.all(|req| req.origin == origin)
88 {
89 let (transactions, response_txs): (Vec<_>, Vec<_>) =
90 batch.into_iter().map(|req| (req.pool_tx, req.response_tx)).unzip();
91
92 let pool_results = pool.add_transactions(origin, transactions).await;
93 for (response_tx, pool_result) in response_txs.into_iter().zip(pool_results) {
94 let _ = response_tx.send(pool_result);
95 }
96 return
97 }
98
99 let (transactions, response_txs): (Vec<_>, Vec<_>) =
100 batch.into_iter().map(|req| ((req.origin, req.pool_tx), req.response_tx)).unzip();
101
102 let pool_results = pool.add_transactions_with_origins(transactions).await;
103 for (response_tx, pool_result) in response_txs.into_iter().zip(pool_results) {
104 let _ = response_tx.send(pool_result);
105 }
106 }
107}
108
109impl<Pool> Future for BatchTxProcessor<Pool>
110where
111 Pool: TransactionPool + 'static,
112{
113 type Output = ();
114
115 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
116 let mut this = self.project();
117
118 loop {
119 ready!(this.request_rx.poll_recv_many(cx, this.buf, *this.max_batch_size));
121
122 if !this.buf.is_empty() {
123 let batch = std::mem::take(this.buf);
124 let pool = this.pool.clone();
125 tokio::spawn(
126 async move {
127 Self::process_batch(&pool, batch).await;
128 }
129 .boxed(),
130 );
131 this.buf.reserve(1);
132
133 continue;
134 }
135
136 return Poll::Pending;
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::test_utils::{testing_pool, MockTransaction};
146 use futures::stream::{FuturesUnordered, StreamExt};
147 use std::time::Duration;
148 use tokio::time::timeout;
149
150 #[tokio::test]
151 async fn test_process_batch() {
152 let pool = testing_pool();
153
154 let mut batch_requests = Vec::new();
155 let mut responses = Vec::new();
156
157 for i in 0..100 {
158 let tx = MockTransaction::legacy().with_nonce(i).with_gas_price(100);
159 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
160
161 batch_requests.push(BatchTxRequest::new(TransactionOrigin::Local, tx, response_tx));
162 responses.push(response_rx);
163 }
164
165 BatchTxProcessor::process_batch(&pool, batch_requests).await;
166
167 for response_rx in responses {
168 let result = timeout(Duration::from_millis(5), response_rx)
169 .await
170 .expect("Timeout waiting for response")
171 .expect("Response channel was closed unexpectedly");
172 assert!(result.is_ok());
173 }
174 }
175
176 #[tokio::test]
177 async fn test_process_batch_mixed_origins() {
178 let pool = testing_pool();
179
180 let mut batch_requests = Vec::new();
181 let mut responses = Vec::new();
182
183 for (nonce, origin) in [
184 (0, TransactionOrigin::Local),
185 (1, TransactionOrigin::External),
186 (2, TransactionOrigin::Private),
187 ] {
188 let tx = MockTransaction::legacy().with_nonce(nonce).with_gas_price(100);
189 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
190
191 batch_requests.push(BatchTxRequest::new(origin, tx, response_tx));
192 responses.push(response_rx);
193 }
194
195 BatchTxProcessor::process_batch(&pool, batch_requests).await;
196
197 for response_rx in responses {
198 let result = timeout(Duration::from_millis(5), response_rx)
199 .await
200 .expect("Timeout waiting for response")
201 .expect("Response channel was closed unexpectedly");
202 assert!(result.is_ok());
203 }
204 }
205
206 #[tokio::test]
207 async fn test_batch_processor() {
208 let pool = testing_pool();
209 let (processor, request_tx) = BatchTxProcessor::new(pool.clone(), 1000);
210
211 let handle = tokio::spawn(processor);
213
214 let mut responses = Vec::new();
215
216 for i in 0..50 {
217 let tx = MockTransaction::legacy().with_nonce(i).with_gas_price(100);
218 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
219
220 request_tx
221 .send(BatchTxRequest::new(TransactionOrigin::Local, tx, response_tx))
222 .expect("Could not send batch tx");
223 responses.push(response_rx);
224 }
225
226 tokio::time::sleep(Duration::from_millis(10)).await;
227
228 for rx in responses {
229 let result = timeout(Duration::from_millis(10), rx)
230 .await
231 .expect("Timeout waiting for response")
232 .expect("Response channel was closed unexpectedly");
233 assert!(result.is_ok());
234 }
235
236 drop(request_tx);
237 handle.abort();
238 }
239
240 #[tokio::test]
241 async fn test_add_transaction() {
242 let pool = testing_pool();
243 let (processor, request_tx) = BatchTxProcessor::new(pool.clone(), 1000);
244
245 let handle = tokio::spawn(processor);
247
248 let mut results = Vec::new();
249 for i in 0..10 {
250 let tx = MockTransaction::legacy().with_nonce(i).with_gas_price(100);
251 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
252 let request = BatchTxRequest::new(TransactionOrigin::Local, tx, response_tx);
253 request_tx.send(request).expect("Could not send batch tx");
254 results.push(response_rx);
255 }
256
257 for res in results {
258 let result = timeout(Duration::from_millis(10), res)
259 .await
260 .expect("Timeout waiting for transaction result");
261 assert!(result.is_ok());
262 }
263
264 handle.abort();
265 }
266
267 #[tokio::test]
268 async fn test_max_batch_size() {
269 let pool = testing_pool();
270 let max_batch_size = 10;
271 let (processor, request_tx) = BatchTxProcessor::new(pool.clone(), max_batch_size);
272
273 let handle = tokio::spawn(processor);
275
276 let mut futures = FuturesUnordered::new();
277 for i in 0..max_batch_size {
278 let tx = MockTransaction::legacy().with_nonce(i as u64).with_gas_price(100);
279 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
280 let request = BatchTxRequest::new(TransactionOrigin::Local, tx, response_tx);
281 let request_tx_clone = request_tx.clone();
282
283 let tx_fut = async move {
284 request_tx_clone.send(request).expect("Could not send batch tx");
285 response_rx.await.expect("Could not receive batch response")
286 };
287 futures.push(tx_fut);
288 }
289
290 while let Some(result) = timeout(Duration::from_millis(5), futures.next())
291 .await
292 .expect("Timeout waiting for transaction result")
293 {
294 assert!(result.is_ok());
295 }
296
297 handle.abort();
298 }
299}