1use crate::{
2 handle::ConnectionHandle,
3 ix::PubSubInstruction,
4 managers::{InFlight, RequestManager, SubscriptionManager},
5 PubSubConnect, PubSubFrontend, RawSubscription,
6};
7use alloy_json_rpc::{Id, PubSubItem, Request, Response, ResponsePayload, RpcError, SubId};
8use alloy_primitives::B256;
9use alloy_transport::{
10 utils::{to_json_raw_value, Spawnable},
11 TransportErrorKind, TransportResult,
12};
13use serde_json::value::RawValue;
14use std::time::Duration;
15use tokio::sync::{mpsc, oneshot};
16
17#[cfg(all(target_family = "wasm", target_os = "unknown"))]
18use wasmtimer::tokio::sleep;
19
20#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
21use tokio::time::sleep;
22
23const MAX_RECONNECT_RETRY_INTERVAL: Duration = Duration::from_secs(30);
24
25#[derive(Debug)]
28pub(crate) struct PubSubService<T> {
29 pub(crate) handle: ConnectionHandle,
31
32 pub(crate) connector: T,
34
35 pub(crate) reqs: mpsc::UnboundedReceiver<PubSubInstruction>,
37
38 pub(crate) subs: SubscriptionManager,
40
41 pub(crate) in_flights: RequestManager,
43}
44
45impl<T: PubSubConnect> PubSubService<T> {
46 pub(crate) async fn connect(connector: T) -> TransportResult<PubSubFrontend> {
48 let handle = connector.connect().await?;
49
50 let (tx, reqs) = mpsc::unbounded_channel();
51 let this = Self {
52 handle,
53 connector,
54 reqs,
55 subs: SubscriptionManager::default(),
56 in_flights: Default::default(),
57 };
58 this.spawn();
59 Ok(PubSubFrontend::new(tx))
60 }
61
62 async fn get_new_backend(&mut self) -> TransportResult<ConnectionHandle> {
64 let mut handle = self.connector.try_reconnect().await?;
65 std::mem::swap(&mut self.handle, &mut handle);
66 Ok(handle)
67 }
68
69 async fn reconnect(&mut self) -> TransportResult<()> {
72 debug!("Reconnecting pubsub service backend");
73
74 let mut old_handle = self.get_new_backend().await?;
75
76 debug!("Draining old backend to_handle");
77
78 while let Ok(item) = old_handle.from_socket.try_recv() {
80 self.handle_item(item)?;
81 }
82
83 old_handle.shutdown();
84
85 debug!(count = self.in_flights.len(), "Reissuing pending requests");
87 for (_, in_flight) in self.in_flights.iter() {
88 let msg = in_flight.request.serialized().to_owned();
89 self.dispatch_request(msg)?;
90 }
91
92 debug!(count = self.subs.len(), "Re-starting active subscriptions");
94
95 self.subs.drop_server_ids();
97
98 for (_, sub) in self.subs.iter() {
100 let req = sub.request().to_owned();
101 let (in_flight, _) = InFlight::new(req.clone(), sub.tx.receiver_count());
102 self.in_flights.insert(in_flight);
103
104 let msg = req.into_serialized();
105 self.dispatch_request(msg)?;
106 }
107
108 Ok(())
109 }
110
111 fn dispatch_request(&self, brv: Box<RawValue>) -> TransportResult<()> {
113 self.handle.to_socket.send(brv).map(drop).map_err(|_| TransportErrorKind::backend_gone())
114 }
115
116 fn service_request(&mut self, in_flight: InFlight) -> TransportResult<()> {
118 let brv = in_flight.request();
119
120 self.dispatch_request(brv.serialized().to_owned())?;
121 self.in_flights.insert(in_flight);
122
123 Ok(())
124 }
125
126 fn service_get_sub(&self, local_id: B256, tx: oneshot::Sender<Option<RawSubscription>>) {
131 let _ = tx.send(self.subs.get_subscription(local_id));
132 }
133
134 fn service_unsubscribe(&mut self, local_id: B256) -> TransportResult<()> {
136 if let Some(server_id) = self.subs.server_id_for(&local_id) {
137 let req = Request::new("eth_unsubscribe", Id::Number(1), [server_id]);
139 let brv = req.serialize().expect("no ser error").take_request();
140
141 self.dispatch_request(brv)?;
142 }
143 self.subs.remove_sub(local_id);
144 Ok(())
145 }
146
147 fn service_ix(&mut self, ix: PubSubInstruction) -> TransportResult<()> {
149 trace!(?ix, "servicing instruction");
150 match ix {
151 PubSubInstruction::Request(in_flight) => self.service_request(in_flight),
152 PubSubInstruction::GetSub(alias, tx) => {
153 self.service_get_sub(alias, tx);
154 Ok(())
155 }
156 PubSubInstruction::Unsubscribe(alias) => self.service_unsubscribe(alias),
157 }
158 }
159
160 fn handle_item(&mut self, item: PubSubItem) -> TransportResult<()> {
162 match item {
163 PubSubItem::Response(resp) => match self.in_flights.handle_response(resp) {
164 Some((server_id, in_flight)) => self.handle_sub_response(in_flight, server_id),
165 None => Ok(()),
166 },
167 PubSubItem::Notification(notification) => {
168 self.subs.notify(notification);
169 Ok(())
170 }
171 }
172 }
173
174 fn handle_sub_response(
176 &mut self,
177 in_flight: InFlight,
178 server_id: SubId,
179 ) -> TransportResult<()> {
180 let request = in_flight.request;
181 let id = request.id().clone();
182
183 let sub = self.subs.upsert(request, server_id, in_flight.channel_size);
184
185 let ser_alias = to_json_raw_value(sub.local_id())?;
187
188 let _ =
191 in_flight.tx.send(Ok(Response { id, payload: ResponsePayload::Success(ser_alias) }));
192
193 Ok(())
194 }
195
196 async fn reconnect_with_retries(&mut self) -> TransportResult<()> {
203 let mut retry_count = 0;
204 let max_retries = self.handle.max_retries;
205 let interval = self.handle.retry_interval;
206 loop {
207 match self.reconnect().await {
208 Ok(()) => break Ok(()),
209 Err(e) => {
210 if matches!(&e, RpcError::Transport(k) if k.is_non_retryable()) {
211 error!("Reconnect aborted (non-retryable), shutting down: {e}");
212 break Err(e);
213 }
214 retry_count += 1;
215 if retry_count >= max_retries {
216 error!("Reconnect failed after {max_retries} attempts, shutting down: {e}");
217 break Err(e);
218 }
219 let retry_interval = reconnect_retry_interval(interval, retry_count);
220 warn!(
221 "Reconnection attempt {retry_count}/{max_retries} failed: {e}. \
222 Retrying in {retry_interval:?}...",
223 );
224 sleep(retry_interval).await;
225 }
226 }
227 }
228 }
229
230 pub(crate) fn spawn(mut self) {
232 let fut = async move {
233 let result: TransportResult<()> = loop {
234 tokio::select! {
238 biased;
239
240 item_opt = self.handle.from_socket.recv() => {
241 if let Some(item) = item_opt {
242 if let Err(e) = self.handle_item(item) {
243 break Err(e)
244 }
245 } else {
246 if let Ok(err) = self.handle.error.try_recv() {
251 if matches!(&err, RpcError::Transport(k) if k.is_non_retryable()) {
252 error!(%err, "Pubsub service backend reported a non-retryable error, shutting down.");
253 break Err(err)
254 }
255 error!(%err, "Pubsub service backend error.");
256 }
257 if let Err(e) = self.reconnect_with_retries().await {
258 break Err(e)
259 }
260 }
261 }
262
263 res = &mut self.handle.error => {
264 let err = res.unwrap_or_else(|_| TransportErrorKind::backend_gone());
269 if matches!(&err, RpcError::Transport(k) if k.is_non_retryable()) {
270 error!(%err, "Pubsub service backend reported a non-retryable error, shutting down.");
271 break Err(err)
272 }
273 error!(%err, "Pubsub service backend error.");
274 if let Err(e) = self.reconnect_with_retries().await {
275 break Err(e)
276 }
277 }
278
279 req_opt = self.reqs.recv() => {
280 if let Some(req) = req_opt {
281 if let Err(err) = self.service_ix(req) {
282 if err
283 .as_transport_err()
284 .is_some_and(TransportErrorKind::is_backend_gone)
285 {
286 if let Err(e) = self.reconnect_with_retries().await {
287 break Err(e)
288 }
289 } else {
290 break Err(err)
291 }
292 }
293 } else {
294 info!("Pubsub service request channel closed. Shutting down.");
295 break Ok(())
296 }
297 }
298 }
299 };
300
301 if let Err(err) = result {
302 error!(%err, "pubsub service reconnection error");
303 }
304 };
305 fut.spawn_task();
306 }
307}
308
309fn reconnect_retry_interval(base_interval: Duration, retry_count: u32) -> Duration {
316 let backoff_multiplier = 1u32.checked_shl(retry_count.saturating_sub(1)).unwrap_or(u32::MAX);
317 let max_interval = base_interval.max(MAX_RECONNECT_RETRY_INTERVAL);
318
319 base_interval.saturating_mul(backoff_multiplier).min(max_interval)
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::ConnectionInterface;
326 use alloy_json_rpc::Request;
327 use std::{
328 sync::{
329 atomic::{AtomicUsize, Ordering},
330 Arc, Mutex,
331 },
332 time::Duration,
333 };
334 use tokio::time::timeout;
335
336 #[derive(Clone, Debug, Default)]
337 struct MockConnect(Arc<Mutex<Option<ConnectionHandle>>>);
338
339 impl PubSubConnect for MockConnect {
340 fn is_local(&self) -> bool {
341 true
342 }
343
344 async fn connect(&self) -> TransportResult<ConnectionHandle> {
345 Err(TransportErrorKind::custom_str("connect is not used in this test"))
346 }
347
348 async fn try_reconnect(&self) -> TransportResult<ConnectionHandle> {
349 self.0
350 .lock()
351 .expect("poisoned mutex")
352 .take()
353 .ok_or_else(|| TransportErrorKind::custom_str("missing mock connection handle"))
354 }
355 }
356
357 #[derive(Clone, Debug, Default)]
360 struct CountingConnect {
361 handle: Arc<Mutex<Option<ConnectionHandle>>>,
362 calls: Arc<AtomicUsize>,
363 }
364
365 impl CountingConnect {
366 fn with_handle(handle: ConnectionHandle) -> Self {
367 Self {
368 handle: Arc::new(Mutex::new(Some(handle))),
369 calls: Arc::new(AtomicUsize::new(0)),
370 }
371 }
372 }
373
374 impl PubSubConnect for CountingConnect {
375 fn is_local(&self) -> bool {
376 true
377 }
378
379 async fn connect(&self) -> TransportResult<ConnectionHandle> {
380 Err(TransportErrorKind::custom_str("connect is not used in this test"))
381 }
382
383 async fn try_reconnect(&self) -> TransportResult<ConnectionHandle> {
384 self.calls.fetch_add(1, Ordering::SeqCst);
385 self.handle
386 .lock()
387 .expect("poisoned mutex")
388 .take()
389 .ok_or_else(|| TransportErrorKind::custom_str("no more handles"))
390 }
391 }
392
393 #[derive(Clone, Debug, Default)]
395 struct NonRetryableConnect(Arc<AtomicUsize>);
396
397 impl PubSubConnect for NonRetryableConnect {
398 fn is_local(&self) -> bool {
399 true
400 }
401
402 async fn connect(&self) -> TransportResult<ConnectionHandle> {
403 Err(TransportErrorKind::non_retryable_str("non-retryable test failure"))
404 }
405
406 async fn try_reconnect(&self) -> TransportResult<ConnectionHandle> {
407 self.0.fetch_add(1, Ordering::SeqCst);
408 Err(TransportErrorKind::non_retryable_str("non-retryable test failure"))
409 }
410 }
411
412 #[test]
413 fn reconnect_retry_interval_uses_capped_exponential_backoff() {
414 let base = Duration::from_secs(1);
415
416 assert_eq!(reconnect_retry_interval(base, 1), Duration::from_secs(1));
417 assert_eq!(reconnect_retry_interval(base, 2), Duration::from_secs(2));
418 assert_eq!(reconnect_retry_interval(base, 3), Duration::from_secs(4));
419 assert_eq!(reconnect_retry_interval(base, 6), Duration::from_secs(30));
420 }
421
422 #[test]
423 fn reconnect_retry_interval_uses_configured_base_interval() {
424 let base = Duration::from_millis(1);
425
426 assert_eq!(reconnect_retry_interval(base, 1), Duration::from_millis(1));
427 assert_eq!(reconnect_retry_interval(base, 2), Duration::from_millis(2));
428 }
429
430 #[test]
431 fn reconnect_retry_interval_does_not_shorten_base_above_cap() {
432 let base = Duration::from_secs(60);
433
434 assert_eq!(reconnect_retry_interval(base, 1), Duration::from_secs(60));
435 assert_eq!(reconnect_retry_interval(base, 2), Duration::from_secs(60));
436 }
437
438 #[tokio::test]
439 async fn reconnects_after_request_dispatch_hits_backend_gone() {
440 let (dead_handle, dead_interface) = ConnectionHandle::new();
441 let ConnectionInterface { from_frontend, to_frontend, error, shutdown } = dead_interface;
442 drop(from_frontend);
443 let _keep_dead_backend_alive = (to_frontend, error, shutdown);
444
445 let (reconnected_handle, mut reconnected_interface) = ConnectionHandle::new();
446 let connector = MockConnect(Arc::new(Mutex::new(Some(reconnected_handle))));
447 let (tx, reqs) = mpsc::unbounded_channel();
448 let service = PubSubService {
449 handle: dead_handle,
450 connector,
451 reqs,
452 subs: SubscriptionManager::default(),
453 in_flights: RequestManager::default(),
454 };
455 service.spawn();
456
457 let first = Request::new("eth_blockNumber", Id::Number(1), ()).serialize().unwrap();
458 let (in_flight, rx) = InFlight::new(first, 16);
459 tx.send(PubSubInstruction::Request(in_flight)).unwrap();
460
461 timeout(Duration::from_secs(1), rx)
462 .await
463 .expect("failed request should resolve promptly")
464 .expect_err("raced request should be dropped when the backend is gone");
465
466 let second = Request::new("eth_chainId", Id::Number(2), ()).serialize().unwrap();
467 let expected = second.serialized().get().to_owned();
468 let (in_flight, _rx) = InFlight::new(second, 16);
469 tx.send(PubSubInstruction::Request(in_flight)).unwrap();
470
471 let dispatched =
472 timeout(Duration::from_secs(1), reconnected_interface.recv_from_frontend())
473 .await
474 .expect("request should be dispatched after reconnect")
475 .expect("new backend should receive the request");
476 assert_eq!(dispatched.get(), expected);
477 }
478
479 #[tokio::test]
480 async fn non_retryable_reconnect_error_short_circuits_retry_loop() {
481 let (dead_handle, dead_interface) = ConnectionHandle::new();
482 let ConnectionInterface { from_frontend, to_frontend, error, shutdown } = dead_interface;
483 drop(from_frontend);
484 let _keep_dead_backend_alive = (to_frontend, error, shutdown);
485
486 let connector = NonRetryableConnect::default();
487 let counter = connector.0.clone();
488 let (tx, reqs) = mpsc::unbounded_channel();
489 let service = PubSubService {
490 handle: dead_handle,
491 connector,
492 reqs,
493 subs: SubscriptionManager::default(),
494 in_flights: RequestManager::default(),
495 };
496 service.spawn();
497
498 let req = Request::new("eth_blockNumber", Id::Number(1), ()).serialize().unwrap();
499 let (in_flight, rx) = InFlight::new(req, 16);
500 tx.send(PubSubInstruction::Request(in_flight)).unwrap();
501
502 timeout(Duration::from_secs(1), rx)
503 .await
504 .expect("non-retryable reconnect should resolve promptly")
505 .expect_err("request should fail when backend is gone and reconnect aborts");
506
507 assert_eq!(counter.load(Ordering::SeqCst), 1);
509 }
510
511 #[tokio::test]
512 async fn non_retryable_close_skips_reconnect_loop() {
513 let (live_handle, live_interface) = ConnectionHandle::new();
517
518 let (spare_handle, _spare_interface) = ConnectionHandle::new();
522 let connector = CountingConnect::with_handle(spare_handle);
523 let calls = connector.calls.clone();
524
525 let (_tx, reqs) = mpsc::unbounded_channel();
526 let service = PubSubService {
527 handle: live_handle,
528 connector,
529 reqs,
530 subs: SubscriptionManager::default(),
531 in_flights: RequestManager::default(),
532 };
533 service.spawn();
534
535 live_interface.close_with_transport_error(TransportErrorKind::non_retryable_str(
537 "deterministic protocol failure",
538 ));
539
540 tokio::time::sleep(Duration::from_millis(50)).await;
542
543 assert_eq!(
544 calls.load(Ordering::SeqCst),
545 0,
546 "non-retryable backend error must not trigger reconnect attempts"
547 );
548 }
549
550 #[tokio::test]
551 async fn default_close_with_error_still_reconnects() {
552 let (live_handle, live_interface) = ConnectionHandle::new();
555
556 let (reconnected_handle, mut reconnected_interface) = ConnectionHandle::new();
557 let connector = CountingConnect::with_handle(reconnected_handle);
558 let calls = connector.calls.clone();
559
560 let (tx, reqs) = mpsc::unbounded_channel();
561 let service = PubSubService {
562 handle: live_handle,
563 connector,
564 reqs,
565 subs: SubscriptionManager::default(),
566 in_flights: RequestManager::default(),
567 };
568 service.spawn();
569
570 live_interface.close_with_error();
572
573 let req = Request::new("eth_chainId", Id::Number(1), ()).serialize().unwrap();
576 let expected = req.serialized().get().to_owned();
577 let (in_flight, _rx) = InFlight::new(req, 16);
578 tx.send(PubSubInstruction::Request(in_flight)).unwrap();
579
580 let dispatched =
581 timeout(Duration::from_secs(1), reconnected_interface.recv_from_frontend())
582 .await
583 .expect("request should be dispatched after reconnect")
584 .expect("new backend should receive the request");
585 assert_eq!(dispatched.get(), expected);
586
587 assert_eq!(
588 calls.load(Ordering::SeqCst),
589 1,
590 "default close_with_error should trigger exactly one reconnect"
591 );
592 }
593}