Skip to main content

forest/rpc/
parallel_batch_layer.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::{borrow::Cow, sync::Arc};
5
6use ahash::HashMap;
7use jsonrpsee::{
8    MethodResponse,
9    core::middleware::{Batch, BatchEntry, Notification},
10    server::{BatchResponseBuilder, middleware::rpc::RpcServiceT},
11    types::{ErrorCode, ErrorObject, Id, Request},
12};
13use tokio::task::JoinSet;
14use tower::Layer;
15
16/// Parallelize batch RPC requests across the `tokio` worker pool.
17///
18/// jsonrpsee processes batches sequentially by default. The
19/// [JSON-RPC spec](https://www.jsonrpc.org/specification#batch) does not
20/// require sequential processing or response ordering, but order is
21/// preserved here to avoid surprising clients.
22#[derive(Clone, derive_more::Constructor)]
23pub(super) struct ParallelBatchLayer {
24    max_response_body_size: usize,
25}
26
27impl<S> Layer<S> for ParallelBatchLayer {
28    type Service = ParallelBatchService<S>;
29
30    fn layer(&self, service: S) -> Self::Service {
31        ParallelBatchService {
32            service: Arc::new(service),
33            max_response_body_size: self.max_response_body_size,
34        }
35    }
36}
37
38#[derive(Clone)]
39pub(super) struct ParallelBatchService<S> {
40    service: Arc<S>,
41    max_response_body_size: usize,
42}
43
44impl<S> RpcServiceT for ParallelBatchService<S>
45where
46    S: RpcServiceT<
47            MethodResponse = MethodResponse,
48            NotificationResponse = MethodResponse,
49            BatchResponse = MethodResponse,
50        > + Send
51        + Sync
52        + 'static,
53{
54    type MethodResponse = S::MethodResponse;
55    type NotificationResponse = S::NotificationResponse;
56    type BatchResponse = S::BatchResponse;
57
58    fn call<'a>(&self, req: Request<'a>) -> impl Future<Output = Self::MethodResponse> + Send + 'a {
59        self.service.call(req)
60    }
61
62    fn batch<'a>(&self, batch: Batch<'a>) -> impl Future<Output = Self::BatchResponse> + Send + 'a {
63        let max = self.max_response_body_size;
64        let mut got_notification = false;
65        // JoinSet aborts in-flight tasks on drop.
66        let mut join_set: JoinSet<(usize, Option<MethodResponse>)> = JoinSet::new();
67        // Lets a panicked call task be turned into a per-entry error with the
68        // original request id.
69        let mut call_meta: HashMap<tokio::task::Id, (usize, Id<'static>)> = HashMap::default();
70        let mut results: Vec<(usize, Option<MethodResponse>)> = Vec::new();
71
72        for (idx, entry) in batch.into_iter().enumerate() {
73            let service = Arc::clone(&self.service);
74            match entry {
75                Ok(BatchEntry::Call(req)) => {
76                    let req_id = req.id().into_owned();
77                    let req = into_owned_request(req);
78                    let handle =
79                        join_set.spawn(async move { (idx, Some(service.call(req).await)) });
80                    call_meta.insert(handle.id(), (idx, req_id));
81                }
82                Ok(BatchEntry::Notification(n)) => {
83                    got_notification = true;
84                    let n = into_owned_notification(n);
85                    join_set.spawn(async move {
86                        service.notification(n).await;
87                        (idx, None)
88                    });
89                }
90                Err(err) => {
91                    let (err, id) = err.into_parts();
92                    results.push((
93                        idx,
94                        Some(MethodResponse::error(id.into_owned(), err.into_owned())),
95                    ));
96                }
97            }
98        }
99
100        async move {
101            results.reserve(join_set.len());
102            while let Some(joined) = join_set.join_next_with_id().await {
103                match joined {
104                    Ok((_, r)) => results.push(r),
105                    // A `JoinError` is a panic or a cancellation. Panics are only turned into an
106                    // error here under `panic = "unwind"`; release builds are `panic = "abort"`
107                    // (see Cargo.toml), where a handler panic aborts the process before it reaches
108                    // this arm. This loop never aborts its own tasks, so cancellation does not occur.
109                    Err(e) => {
110                        if let Some((idx, req_id)) = call_meta.remove(&e.id()) {
111                            tracing::error!(idx, "RPC call failed in batch entry: {e}");
112                            let err = ErrorObject::owned::<()>(
113                                ErrorCode::InternalError.code(),
114                                "RPC handler panicked",
115                                None,
116                            );
117                            results.push((idx, Some(MethodResponse::error(req_id, err))));
118                        } else {
119                            tracing::error!("RPC notification failed in batch entry: {e}");
120                        }
121                    }
122                }
123            }
124            results.sort_by_key(|(idx, _)| *idx);
125
126            let mut batch_rp = BatchResponseBuilder::new_with_limit(max);
127            for (_, rp) in results {
128                if let Some(rp) = rp
129                    && let Err(err) = batch_rp.append(rp)
130                {
131                    return err;
132                }
133            }
134
135            // Empty builder + at least one notification is the spec's
136            // "no response" case for a notification-only batch.
137            if batch_rp.is_empty() && got_notification {
138                MethodResponse::notification()
139            } else {
140                MethodResponse::from_batch(batch_rp.finish())
141            }
142        }
143    }
144
145    fn notification<'a>(
146        &self,
147        n: Notification<'a>,
148    ) -> impl Future<Output = Self::NotificationResponse> + Send + 'a {
149        self.service.notification(n)
150    }
151}
152
153fn into_owned_request(req: Request<'_>) -> Request<'static> {
154    Request {
155        jsonrpc: req.jsonrpc,
156        id: req.id.into_owned(),
157        method: Cow::Owned(req.method.into_owned()),
158        params: req.params.map(|p| Cow::Owned(p.into_owned())),
159        extensions: req.extensions,
160    }
161}
162
163fn into_owned_notification(n: Notification<'_>) -> Notification<'static> {
164    Notification {
165        jsonrpc: n.jsonrpc,
166        method: Cow::Owned(n.method.into_owned()),
167        params: n.params.map(|p| Cow::Owned(p.into_owned())),
168        extensions: n.extensions,
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use jsonrpsee::core::middleware::BatchEntryErr;
176    use jsonrpsee::server::ResponsePayload;
177    use jsonrpsee::types::{Extensions, TwoPointZero};
178    use std::time::Duration;
179
180    const MAX_RESP: usize = 1024 * 1024;
181
182    /// Method conventions used by tests:
183    ///   "ok"        – success response carrying the method name.
184    ///   "slow:<ms>" – sleep, then succeed.
185    ///   "panic"     – panic inside the call task.
186    #[derive(Clone, Default)]
187    struct TestService;
188
189    impl RpcServiceT for TestService {
190        type MethodResponse = MethodResponse;
191        type NotificationResponse = MethodResponse;
192        type BatchResponse = MethodResponse;
193
194        fn call<'a>(
195            &self,
196            req: Request<'a>,
197        ) -> impl Future<Output = Self::MethodResponse> + Send + 'a {
198            let id = req.id().into_owned();
199            let method = req.method_name().to_string();
200            async move {
201                if method == "panic" {
202                    panic!("test panic");
203                }
204                if let Some(rest) = method.strip_prefix("slow:") {
205                    let ms: u64 = rest.parse().unwrap();
206                    tokio::time::sleep(Duration::from_millis(ms)).await;
207                }
208                MethodResponse::response(id, ResponsePayload::success(method), MAX_RESP)
209            }
210        }
211
212        // `async fn` form drops the explicit `'a` capture the trait wants,
213        // and the `manual_async_fn` lint fires on trivial `async {}` bodies.
214        #[expect(clippy::manual_async_fn, reason = "trait demands explicit 'a")]
215        fn batch<'a>(
216            &self,
217            _b: Batch<'a>,
218        ) -> impl Future<Output = Self::BatchResponse> + Send + 'a {
219            async { unreachable!("ParallelBatchLayer overrides this") }
220        }
221
222        #[expect(clippy::manual_async_fn, reason = "trait demands explicit 'a")]
223        fn notification<'a>(
224            &self,
225            _n: Notification<'a>,
226        ) -> impl Future<Output = Self::NotificationResponse> + Send + 'a {
227            async { MethodResponse::notification() }
228        }
229    }
230
231    fn layer() -> ParallelBatchService<TestService> {
232        ParallelBatchService {
233            service: Arc::new(TestService),
234            max_response_body_size: MAX_RESP,
235        }
236    }
237
238    fn call(id: u64, method: &str) -> Request<'static> {
239        Request::owned(method.to_string(), None, Id::Number(id))
240    }
241
242    fn notification(method: &str) -> Notification<'static> {
243        Notification {
244            jsonrpc: TwoPointZero,
245            method: Cow::Owned(method.to_string()),
246            params: None,
247            extensions: Extensions::new(),
248        }
249    }
250
251    fn as_array(rp: &MethodResponse) -> Vec<serde_json::Value> {
252        serde_json::from_str::<Vec<serde_json::Value>>(rp.as_json().get()).unwrap()
253    }
254
255    #[tokio::test]
256    async fn preserves_order_under_heterogeneous_latency() {
257        let svc = layer();
258        let batch = Batch::from(vec![
259            Ok(BatchEntry::Call(call(1, "slow:50"))),
260            Ok(BatchEntry::Call(call(2, "ok"))),
261            Ok(BatchEntry::Call(call(3, "slow:25"))),
262        ]);
263        let arr = as_array(&svc.batch(batch).await);
264        assert_eq!(arr.len(), 3);
265        assert_eq!(arr[0]["id"], 1);
266        assert_eq!(arr[1]["id"], 2);
267        assert_eq!(arr[2]["id"], 3);
268    }
269
270    #[tokio::test]
271    async fn panicked_call_yields_per_entry_error() {
272        let svc = layer();
273        let batch = Batch::from(vec![
274            Ok(BatchEntry::Call(call(1, "ok"))),
275            Ok(BatchEntry::Call(call(2, "panic"))),
276            Ok(BatchEntry::Call(call(3, "ok"))),
277        ]);
278        let arr = as_array(&svc.batch(batch).await);
279        assert_eq!(arr.len(), 3);
280        assert_eq!(arr[0]["id"], 1);
281        assert!(arr[0]["result"].is_string(), "first entry should succeed");
282        assert_eq!(arr[1]["id"], 2);
283        assert!(
284            arr[1]["error"].is_object(),
285            "panicked entry must carry its own error"
286        );
287        assert_eq!(arr[2]["id"], 3);
288        assert!(arr[2]["result"].is_string(), "third entry should succeed");
289    }
290
291    #[tokio::test]
292    async fn notification_only_batch_returns_notification() {
293        let svc = layer();
294        let batch = Batch::from(vec![Ok(BatchEntry::Notification(notification("ok")))]);
295        let resp = svc.batch(batch).await;
296        assert!(resp.is_notification());
297    }
298
299    #[tokio::test]
300    async fn entry_err_preserves_index() {
301        let svc = layer();
302        let batch = Batch::from(vec![
303            Ok(BatchEntry::Call(call(1, "ok"))),
304            Err(BatchEntryErr::new(
305                Id::Number(2),
306                ErrorObject::from(ErrorCode::InvalidRequest),
307            )),
308            Ok(BatchEntry::Call(call(3, "ok"))),
309        ]);
310        let arr = as_array(&svc.batch(batch).await);
311        assert_eq!(arr.len(), 3);
312        assert_eq!(arr[0]["id"], 1);
313        assert_eq!(arr[1]["id"], 2);
314        assert!(arr[1]["error"].is_object());
315        assert_eq!(arr[2]["id"], 3);
316    }
317}