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) if e.is_panic() => {
106 if let Some((idx, req_id)) = call_meta.remove(&e.id()) {
107 tracing::error!(idx, "RPC call panicked in batch entry");
108 let err = ErrorObject::owned::<()>(
109 ErrorCode::InternalError.code(),
110 "RPC handler panicked",
111 None,
112 );
113 results.push((idx, Some(MethodResponse::error(req_id, err))));
114 } else {
115 tracing::error!("RPC notification panicked in batch entry");
116 }
117 }
118 Err(_) => unreachable!("JoinSet only cancels tasks on drop"),
119 }
120 }
121 results.sort_by_key(|(idx, _)| *idx);
122
123 let mut batch_rp = BatchResponseBuilder::new_with_limit(max);
124 for (_, rp) in results {
125 if let Some(rp) = rp
126 && let Err(err) = batch_rp.append(rp)
127 {
128 return err;
129 }
130 }
131
132 if batch_rp.is_empty() && got_notification {
135 MethodResponse::notification()
136 } else {
137 MethodResponse::from_batch(batch_rp.finish())
138 }
139 }
140 }
141
142 fn notification<'a>(
143 &self,
144 n: Notification<'a>,
145 ) -> impl Future<Output = Self::NotificationResponse> + Send + 'a {
146 self.service.notification(n)
147 }
148}
149
150fn into_owned_request(req: Request<'_>) -> Request<'static> {
151 Request {
152 jsonrpc: req.jsonrpc,
153 id: req.id.into_owned(),
154 method: Cow::Owned(req.method.into_owned()),
155 params: req.params.map(|p| Cow::Owned(p.into_owned())),
156 extensions: req.extensions,
157 }
158}
159
160fn into_owned_notification(n: Notification<'_>) -> Notification<'static> {
161 Notification {
162 jsonrpc: n.jsonrpc,
163 method: Cow::Owned(n.method.into_owned()),
164 params: n.params.map(|p| Cow::Owned(p.into_owned())),
165 extensions: n.extensions,
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use jsonrpsee::core::middleware::BatchEntryErr;
173 use jsonrpsee::server::ResponsePayload;
174 use jsonrpsee::types::{Extensions, TwoPointZero};
175 use std::time::Duration;
176
177 const MAX_RESP: usize = 1024 * 1024;
178
179 #[derive(Clone, Default)]
184 struct TestService;
185
186 impl RpcServiceT for TestService {
187 type MethodResponse = MethodResponse;
188 type NotificationResponse = MethodResponse;
189 type BatchResponse = MethodResponse;
190
191 fn call<'a>(
192 &self,
193 req: Request<'a>,
194 ) -> impl Future<Output = Self::MethodResponse> + Send + 'a {
195 let id = req.id().into_owned();
196 let method = req.method_name().to_string();
197 async move {
198 if method == "panic" {
199 panic!("test panic");
200 }
201 if let Some(rest) = method.strip_prefix("slow:") {
202 let ms: u64 = rest.parse().unwrap();
203 tokio::time::sleep(Duration::from_millis(ms)).await;
204 }
205 MethodResponse::response(id, ResponsePayload::success(method), MAX_RESP)
206 }
207 }
208
209 #[expect(clippy::manual_async_fn, reason = "trait demands explicit 'a")]
212 fn batch<'a>(
213 &self,
214 _b: Batch<'a>,
215 ) -> impl Future<Output = Self::BatchResponse> + Send + 'a {
216 async { unreachable!("ParallelBatchLayer overrides this") }
217 }
218
219 #[expect(clippy::manual_async_fn, reason = "trait demands explicit 'a")]
220 fn notification<'a>(
221 &self,
222 _n: Notification<'a>,
223 ) -> impl Future<Output = Self::NotificationResponse> + Send + 'a {
224 async { MethodResponse::notification() }
225 }
226 }
227
228 fn layer() -> ParallelBatchService<TestService> {
229 ParallelBatchService {
230 service: Arc::new(TestService),
231 max_response_body_size: MAX_RESP,
232 }
233 }
234
235 fn call(id: u64, method: &str) -> Request<'static> {
236 Request::owned(method.to_string(), None, Id::Number(id))
237 }
238
239 fn notification(method: &str) -> Notification<'static> {
240 Notification {
241 jsonrpc: TwoPointZero,
242 method: Cow::Owned(method.to_string()),
243 params: None,
244 extensions: Extensions::new(),
245 }
246 }
247
248 fn as_array(rp: &MethodResponse) -> Vec<serde_json::Value> {
249 serde_json::from_str::<Vec<serde_json::Value>>(rp.as_json().get()).unwrap()
250 }
251
252 #[tokio::test]
253 async fn preserves_order_under_heterogeneous_latency() {
254 let svc = layer();
255 let batch = Batch::from(vec![
256 Ok(BatchEntry::Call(call(1, "slow:50"))),
257 Ok(BatchEntry::Call(call(2, "ok"))),
258 Ok(BatchEntry::Call(call(3, "slow:25"))),
259 ]);
260 let arr = as_array(&svc.batch(batch).await);
261 assert_eq!(arr.len(), 3);
262 assert_eq!(arr[0]["id"], 1);
263 assert_eq!(arr[1]["id"], 2);
264 assert_eq!(arr[2]["id"], 3);
265 }
266
267 #[tokio::test]
268 async fn panicked_call_yields_per_entry_error() {
269 let svc = layer();
270 let batch = Batch::from(vec![
271 Ok(BatchEntry::Call(call(1, "ok"))),
272 Ok(BatchEntry::Call(call(2, "panic"))),
273 Ok(BatchEntry::Call(call(3, "ok"))),
274 ]);
275 let arr = as_array(&svc.batch(batch).await);
276 assert_eq!(arr.len(), 3);
277 assert_eq!(arr[0]["id"], 1);
278 assert!(arr[0]["result"].is_string(), "first entry should succeed");
279 assert_eq!(arr[1]["id"], 2);
280 assert!(
281 arr[1]["error"].is_object(),
282 "panicked entry must carry its own error"
283 );
284 assert_eq!(arr[2]["id"], 3);
285 assert!(arr[2]["result"].is_string(), "third entry should succeed");
286 }
287
288 #[tokio::test]
289 async fn notification_only_batch_returns_notification() {
290 let svc = layer();
291 let batch = Batch::from(vec![Ok(BatchEntry::Notification(notification("ok")))]);
292 let resp = svc.batch(batch).await;
293 assert!(resp.is_notification());
294 }
295
296 #[tokio::test]
297 async fn entry_err_preserves_index() {
298 let svc = layer();
299 let batch = Batch::from(vec![
300 Ok(BatchEntry::Call(call(1, "ok"))),
301 Err(BatchEntryErr::new(
302 Id::Number(2),
303 ErrorObject::from(ErrorCode::InvalidRequest),
304 )),
305 Ok(BatchEntry::Call(call(3, "ok"))),
306 ]);
307 let arr = as_array(&svc.batch(batch).await);
308 assert_eq!(arr.len(), 3);
309 assert_eq!(arr[0]["id"], 1);
310 assert_eq!(arr[1]["id"], 2);
311 assert!(arr[1]["error"].is_object());
312 assert_eq!(arr[2]["id"], 3);
313 }
314}