1use 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#[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 let mut join_set: JoinSet<(usize, Option<MethodResponse>)> = JoinSet::new();
67 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 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 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 #[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 #[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}