Skip to main content

reth_transaction_pool/
batcher.rs

1//! Transaction batching for `Pool` insertion for high-throughput scenarios
2//!
3//! This module provides transaction batching logic to reduce lock contention when processing
4//! many concurrent transaction pool insertions.
5
6use 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/// A single batch transaction request
19#[derive(Debug)]
20pub struct BatchTxRequest<T: PoolTransaction> {
21    /// Origin of the transaction (e.g. Local, External)
22    origin: TransactionOrigin,
23    /// Tx to be inserted in to the pool
24    pool_tx: T,
25    /// Channel to send result back to caller
26    response_tx: oneshot::Sender<Result<AddedTransactionOutcome, PoolError>>,
27}
28
29impl<T> BatchTxRequest<T>
30where
31    T: PoolTransaction,
32{
33    /// Create a new batch transaction request
34    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/// Transaction batch processor that handles batch processing
44#[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    /// Create a new `BatchTxProcessor`
59    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    /// Process a batch of transaction requests with per-transaction origins
77    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        // Same-origin batches can skip the per-transaction origins allocation.
85        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            // Drain all available requests from the receiver
120            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            // No requests available, return Pending to wait for more
137            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        // Spawn the processor
212        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        // Spawn the processor
246        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        // Spawn batch processor with threshold
274        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}