1use {
2 crate::{
3 dedup::{DedupState, DedupStream},
4 GeyserGrpcClientError, InterceptorXToken, ReconnectConfig,
5 },
6 arc_swap::ArcSwap,
7 futures::{channel::mpsc, sink::SinkExt, stream::Stream},
8 std::{
9 error::Error,
10 future::Future,
11 pin::Pin,
12 sync::{Arc, Mutex},
13 task::{Context, Poll},
14 time::Duration,
15 },
16 tonic::{
17 codec::CompressionEncoding, metadata::AsciiMetadataValue, transport::Endpoint, Code,
18 Status, Streaming,
19 },
20 yellowstone_grpc_proto::{
21 geyser::geyser_client::GeyserClient,
22 prelude::{subscribe_update::UpdateOneof, SubscribeRequest, SubscribeUpdate},
23 },
24};
25
26const CHECKPOINT_SLOT_BUFFER: u64 = 2;
30
31pub const AUTORECONNECT_FILTER_KEY: &str = "__autoreconnect";
32
33type ConnectFuture<S> =
34 Pin<Box<dyn Future<Output = Result<DedupStream<S>, GeyserGrpcClientError>> + Send + 'static>>;
35
36#[derive(Debug, Clone)]
37pub struct Backoff {
38 pub initial_interval: Duration,
39 pub multiplier: f64,
40 pub max_retries: u32,
41}
42
43pub(crate) enum ErrorCategory<E> {
44 Retryable(E),
45 Unrecoverable(E),
46}
47
48impl<E> ErrorCategory<E> {
49 const fn is_retryable(&self) -> bool {
50 matches!(self, Self::Retryable(_))
51 }
52
53 fn into_inner(self) -> E {
54 match self {
55 Self::Retryable(e) | Self::Unrecoverable(e) => e,
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
61struct BackoffInstance {
63 current_interval: Duration,
64 attempts: u32,
65 multiplier: f64,
66 max_retries: u32,
67}
68
69impl BackoffInstance {
70 const fn new(config: &Backoff) -> Self {
71 Self {
72 current_interval: config.initial_interval,
73 attempts: 0,
74 multiplier: config.multiplier,
75 max_retries: config.max_retries,
76 }
77 }
78
79 const fn exhausted(&self) -> bool {
81 self.attempts >= self.max_retries
82 }
83
84 fn advance(&mut self) {
86 self.attempts += 1;
87 self.current_interval = self.current_interval.mul_f64(self.multiplier);
88 }
89}
90
91impl Iterator for BackoffInstance {
92 type Item = Duration;
93
94 fn next(&mut self) -> Option<Self::Item> {
95 if self.exhausted() {
96 None
97 } else {
98 let interval = self.current_interval;
99 self.advance();
100 Some(interval)
101 }
102 }
103}
104
105impl Backoff {
106 const fn default_initial_interval() -> Duration {
107 Duration::from_millis(10)
108 }
109
110 const fn default_multiplier() -> f64 {
111 2.0
112 }
113
114 const fn default_max_retries() -> u32 {
115 3
116 }
117
118 pub const fn new(initial_interval: Duration, multiplier: f64, max_retries: u32) -> Self {
120 Self {
121 initial_interval,
122 multiplier,
123 max_retries,
124 }
125 }
126
127 const fn instance(&self) -> BackoffInstance {
128 BackoffInstance::new(self)
129 }
130
131 pub(crate) fn retry<F, Fut, T, E>(&self, mut operation: F) -> impl Future<Output = Result<T, E>>
136 where
137 F: FnMut() -> Fut,
138 Fut: Future<Output = Result<T, ErrorCategory<E>>>,
139 {
140 let mut state = self.instance();
141 async move {
142 loop {
143 match operation().await {
144 Ok(value) => return Ok(value),
145 Err(error) => {
146 if !error.is_retryable() {
147 return Err(error.into_inner());
148 }
149
150 let Some(sleep_for) = state.next() else {
151 return Err(error.into_inner());
152 };
153
154 tokio::time::sleep(sleep_for).await;
155 }
156 }
157 }
158 }
159 }
160}
161
162impl Default for Backoff {
163 fn default() -> Self {
164 Self::new(
165 Self::default_initial_interval(),
166 Self::default_multiplier(),
167 Self::default_max_retries(),
168 )
169 }
170}
171
172pub trait GrpcConnector: Clone + Send + Sync + 'static {
174 type Stream: Stream<Item = Result<SubscribeUpdate, Status>> + Unpin + Send + 'static;
175
176 type ConnectError: std::error::Error + Send + Sync + 'static;
177
178 type ConnectFuture: Future<Output = Result<Self::Stream, Self::ConnectError>> + Send + 'static;
179
180 fn connect(
183 &self,
184 request: Arc<SubscribeRequest>,
185 from_slot: Option<u64>,
186 ) -> Self::ConnectFuture;
187}
188
189#[derive(Clone)]
193pub struct TonicGrpcConnector {
194 backoff: Backoff,
195 request_sink: Arc<Mutex<mpsc::Sender<SubscribeRequest>>>,
196 endpoint: Endpoint,
197 x_token: Option<AsciiMetadataValue>,
198 x_request_snapshot: bool,
199 send_compressed: Option<CompressionEncoding>,
200 accept_compressed: Option<CompressionEncoding>,
201 max_decoding_message_size: Option<usize>,
202 max_encoding_message_size: Option<usize>,
203}
204
205#[derive(Debug, Clone)]
206pub struct TonicGeyserClientOptions {
207 pub x_request_snapshot: bool,
208 pub send_compressed: Option<CompressionEncoding>,
209 pub accept_compressed: Option<CompressionEncoding>,
210 pub max_decoding_message_size: Option<usize>,
211 pub max_encoding_message_size: Option<usize>,
212}
213
214impl Default for TonicGeyserClientOptions {
215 fn default() -> Self {
216 Self {
217 x_request_snapshot: false,
218 send_compressed: None,
219 accept_compressed: None,
220 max_decoding_message_size: Some(50_000_000), max_encoding_message_size: Some(50_000_000),
222 }
223 }
224}
225
226impl TonicGrpcConnector {
227 pub const fn new(
228 endpoint: Endpoint,
229 config: ReconnectConfig,
230 x_token: Option<AsciiMetadataValue>,
231 options: TonicGeyserClientOptions,
232 request_sink: Arc<Mutex<mpsc::Sender<SubscribeRequest>>>,
233 ) -> Self {
234 Self {
235 backoff: config.backoff,
236 request_sink,
237 endpoint,
238 x_token,
239 x_request_snapshot: options.x_request_snapshot,
240 send_compressed: options.send_compressed,
241 accept_compressed: options.accept_compressed,
242 max_decoding_message_size: options.max_decoding_message_size,
243 max_encoding_message_size: options.max_encoding_message_size,
244 }
245 }
246}
247
248impl GrpcConnector for TonicGrpcConnector {
249 type Stream = Streaming<SubscribeUpdate>;
250 type ConnectError = GeyserGrpcClientError;
251 type ConnectFuture =
252 Pin<Box<dyn Future<Output = Result<Self::Stream, Self::ConnectError>> + Send + 'static>>;
253
254 fn connect(
255 &self,
256 request: Arc<SubscribeRequest>,
257 from_slot: Option<u64>,
258 ) -> Self::ConnectFuture {
259 let backoff = self.backoff.clone();
260 let endpoint = self.endpoint.clone();
261 let request_sink = Arc::clone(&self.request_sink);
262 let x_token = self.x_token.clone();
263 let x_request_snapshot = self.x_request_snapshot;
264 let send_compressed = self.send_compressed;
265 let accept_compressed = self.accept_compressed;
266 let max_decoding_message_size = self.max_decoding_message_size;
267 let max_encoding_message_size = self.max_encoding_message_size;
268 let base_request = (*request).clone();
269
270 let fut = backoff.retry(move || {
271 let endpoint = endpoint.clone();
272 let request_sink = Arc::clone(&request_sink);
273 let x_token = x_token.clone();
274 let mut request = base_request.clone();
275 async move {
276 request.from_slot = from_slot;
277
278 let channel = endpoint
279 .connect()
280 .await
281 .map_err(GeyserGrpcClientError::TransportError)
282 .map_err(ErrorCategory::Retryable)?;
283
284 let interceptor = InterceptorXToken {
285 x_token,
286 x_request_snapshot,
287 };
288
289 let mut geyser =
290 GeyserClient::with_interceptor(channel.clone(), interceptor.clone());
291 if let Some(encoding) = send_compressed {
292 geyser = geyser.send_compressed(encoding);
293 }
294 if let Some(encoding) = accept_compressed {
295 geyser = geyser.accept_compressed(encoding);
296 }
297 if let Some(limit) = max_decoding_message_size {
298 geyser = geyser.max_decoding_message_size(limit);
299 }
300 if let Some(limit) = max_encoding_message_size {
301 geyser = geyser.max_encoding_message_size(limit);
302 }
303
304 let (mut subscribe_tx, subscribe_rx) = mpsc::channel(1000);
305 subscribe_tx
306 .send(request)
307 .await
308 .expect("channel cannot be disconnected or full at this point");
309
310 let mut tonic_request = tonic::Request::new(subscribe_rx);
311 if let Some(slot) = from_slot {
312 tonic_request.metadata_mut().insert(
313 "x-min-context-slot",
314 slot.to_string().parse().expect("slot is valid metadata"),
315 );
316 }
317
318 let response = geyser
319 .subscribe(tonic_request)
320 .await
321 .map_err(GeyserGrpcClientError::from)
322 .map_err(|e| {
323 if is_recoverable_client_error(&e) {
324 ErrorCategory::Retryable(e)
325 } else {
326 ErrorCategory::Unrecoverable(e)
327 }
328 })?;
329
330 *request_sink.lock().expect("request sink mutex poisoned") = subscribe_tx;
333
334 Ok(response.into_inner())
335 }
336 });
337
338 Box::pin(fut)
339 }
340}
341
342pub struct AutoReconnect<GrpcStream, Connector> {
350 request: Arc<ArcSwap<SubscribeRequest>>,
351 last_checkpoint: Option<u64>,
352 stop: bool,
353 connector: Connector,
354 backoff: Backoff,
355 inner_stream: Option<DedupStream<GrpcStream>>,
356 pending_connecting_task: Option<ConnectFuture<GrpcStream>>,
357}
358
359impl<S, Connector> AutoReconnect<S, Connector>
360where
361 S: Stream<Item = Result<SubscribeUpdate, Status>> + Unpin + Send + 'static,
362 Connector: GrpcConnector<Stream = S, ConnectError = GeyserGrpcClientError>,
363{
364 pub fn new(
365 stream: DedupStream<S>,
366 connector: Connector,
367 request: Arc<ArcSwap<SubscribeRequest>>,
368 backoff: Backoff,
369 ) -> Self {
370 Self {
371 request,
372 last_checkpoint: None,
373 inner_stream: Some(stream),
374 pending_connecting_task: None,
375 stop: false,
376 connector,
377 backoff,
378 }
379 }
380
381 fn make_connection_future(&self, dedup_state: DedupState) -> ConnectFuture<S> {
382 let connector = self.connector.clone();
383 let request = self.request.load_full();
384 let from_slot = self.last_checkpoint;
385 let fut = async move {
386 let stream = connector.connect(request, from_slot).await?;
387 Ok(DedupStream::new(stream, dedup_state))
388 };
389 Box::pin(fut)
390 }
391}
392
393impl<S, Connector> Stream for AutoReconnect<S, Connector>
394where
395 S: Stream<Item = Result<SubscribeUpdate, Status>> + Unpin + Send + 'static,
396 Connector: GrpcConnector<Stream = S, ConnectError = GeyserGrpcClientError> + Unpin,
397{
398 type Item = Result<SubscribeUpdate, Status>;
399
400 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
408 if self.stop {
409 return Poll::Ready(None);
410 }
411 let me = self.get_mut();
412 while !me.stop {
413 if let Some(mut stream) = me.inner_stream.take() {
414 match Pin::new(&mut stream).poll_next(cx) {
415 Poll::Ready(Some(Ok(mut msg))) => {
416 if let Some(UpdateOneof::BlockMeta(_)) = msg.update_oneof.as_ref() {
417 if let Some(slot) = extract_slot(&msg) {
418 me.last_checkpoint =
424 Some(slot.saturating_sub(CHECKPOINT_SLOT_BUFFER));
425 }
426 }
427
428 if !msg.filters.is_empty()
432 && msg.filters.iter().all(|f| f == AUTORECONNECT_FILTER_KEY)
433 {
434 me.inner_stream = Some(stream);
436 continue;
437 }
438
439 msg.filters.retain(|f| f != AUTORECONNECT_FILTER_KEY);
441
442 me.inner_stream = Some(stream);
443 return Poll::Ready(Some(Ok(msg)));
444 }
445 Poll::Ready(Some(Err(status))) if is_recoverable_status_code(status.code()) => {
446 if status.code() == Code::OutOfRange {
450 me.last_checkpoint = None;
451 }
452
453 if me.backoff.max_retries == 0 {
454 me.stop = true;
455 return Poll::Ready(Some(Err(status)));
456 }
457
458 log::warn!(
459 "stream error: {status}. starting reconnect from slot {:?}",
460 me.last_checkpoint
461 );
462
463 let mut dedup_state = stream.state;
464 dedup_state.prepare_for_replay();
465 me.pending_connecting_task = Some(me.make_connection_future(dedup_state));
466 }
467 Poll::Ready(Some(Err(e))) => {
468 me.stop = true;
469 return Poll::Ready(Some(Err(e)));
470 }
471 Poll::Ready(None) => {
472 me.stop = true;
473 return Poll::Ready(None);
474 }
475 Poll::Pending => {
476 me.inner_stream = Some(stream);
477 return Poll::Pending;
478 }
479 }
480 }
481
482 if let Some(mut fut) = me.pending_connecting_task.take() {
483 match fut.as_mut().poll(cx) {
484 Poll::Ready(result) => match result {
485 Ok(stream) => {
486 me.inner_stream = Some(stream);
487 }
488 Err(error) => {
489 me.stop = true;
490 return Poll::Ready(Some(Err(Status::internal(format!(
491 "reconnect failed: {error}",
492 )))));
493 }
494 },
495 Poll::Pending => {
496 me.pending_connecting_task = Some(fut);
497 return Poll::Pending;
498 }
499 }
500 }
501 assert!(
502 me.inner_stream.is_some() || me.pending_connecting_task.is_some() || me.stop,
503 "must have either an active stream, a pending connecting task, or be stopped"
504 );
505 }
506
507 Poll::Ready(None)
508 }
509}
510
511fn is_recoverable_client_error(err: &GeyserGrpcClientError) -> bool {
513 match err {
514 GeyserGrpcClientError::TonicStatus(status) => {
515 if let Some(source) = status.source() {
517 if source.downcast_ref::<tonic::transport::Error>().is_some() {
518 return true;
519 }
520 }
521 is_recoverable_status_code(status.code())
522 }
523 GeyserGrpcClientError::TransportError(_) => true,
524 }
525}
526
527const fn is_recoverable_status_code(code: Code) -> bool {
529 matches!(
530 code,
531 Code::Cancelled
532 | Code::Unknown
533 | Code::DeadlineExceeded
534 | Code::ResourceExhausted
535 | Code::Aborted
536 | Code::Internal
537 | Code::Unavailable
538 | Code::DataLoss
539 | Code::OutOfRange
540 )
541}
542
543pub(crate) fn extract_slot(msg: &SubscribeUpdate) -> Option<u64> {
545 match msg.update_oneof.as_ref()? {
546 UpdateOneof::Account(m) => Some(m.slot),
547 UpdateOneof::Slot(m) => Some(m.slot),
548 UpdateOneof::Transaction(m) => Some(m.slot),
549 UpdateOneof::Block(m) => Some(m.slot),
550 UpdateOneof::BlockMeta(m) => Some(m.slot),
551 UpdateOneof::Entry(m) => Some(m.slot),
552 UpdateOneof::TransactionStatus(m) => Some(m.slot),
553 UpdateOneof::Ping(_) | UpdateOneof::Pong(_) => None,
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use {
560 super::*,
561 futures::{stream, StreamExt},
562 std::{
563 collections::VecDeque,
564 sync::{Arc, Mutex},
565 },
566 tonic::Code,
567 yellowstone_grpc_proto::{
568 geyser::{
569 SubscribeUpdateAccount, SubscribeUpdateAccountInfo, SubscribeUpdateBlockMeta,
570 },
571 prelude::{subscribe_update::UpdateOneof, SubscribeUpdatePing, SubscribeUpdateSlot},
572 },
573 };
574
575 #[derive(Clone)]
576 struct MockGrpcConnector {
577 plans: Arc<Mutex<VecDeque<ConnectPlan>>>,
578 from_slot_calls: Arc<Mutex<Vec<Option<u64>>>>,
579 }
580
581 struct ConnectPlan {
582 expected_from_slot: Option<Option<u64>>,
583 result: Result<Vec<Result<SubscribeUpdate, Status>>, GeyserGrpcClientError>,
584 }
585
586 impl MockGrpcConnector {
587 fn new(plans: Vec<ConnectPlan>) -> Self {
588 Self {
589 plans: Arc::new(Mutex::new(plans.into())),
590 from_slot_calls: Arc::new(Mutex::new(Vec::new())),
591 }
592 }
593
594 fn calls(&self) -> Vec<Option<u64>> {
595 self.from_slot_calls
596 .lock()
597 .expect("calls mutex poisoned")
598 .clone()
599 }
600 }
601
602 impl GrpcConnector for MockGrpcConnector {
603 type Stream = futures::stream::BoxStream<'static, Result<SubscribeUpdate, Status>>;
604 type ConnectError = GeyserGrpcClientError;
605 type ConnectFuture = Pin<
606 Box<dyn Future<Output = Result<Self::Stream, Self::ConnectError>> + Send + 'static>,
607 >;
608
609 fn connect(
610 &self,
611 _request: Arc<SubscribeRequest>,
612 from_slot: Option<u64>,
613 ) -> Self::ConnectFuture {
614 let mut plans = self.plans.lock().expect("plans mutex poisoned");
615 let plan = plans
616 .pop_front()
617 .expect("unexpected connect call without a test plan");
618 drop(plans);
619
620 self.from_slot_calls
621 .lock()
622 .expect("calls mutex poisoned")
623 .push(from_slot);
624
625 if let Some(expected_from_slot) = plan.expected_from_slot {
626 assert_eq!(
627 from_slot, expected_from_slot,
628 "unexpected reconnect from_slot"
629 );
630 }
631
632 Box::pin(async move {
633 match plan.result {
634 Ok(items) => Ok(stream::iter(items).boxed()),
635 Err(err) => Err(err),
636 }
637 })
638 }
639 }
640
641 fn backoff_with_retries(max_retries: u32) -> Backoff {
642 Backoff::new(Duration::from_millis(0), 1.0, max_retries)
643 }
644
645 fn request_state(request: SubscribeRequest) -> Arc<ArcSwap<SubscribeRequest>> {
646 Arc::new(ArcSwap::new(Arc::new(request)))
647 }
648
649 #[test]
650 fn test_backoff_default() {
651 let backoff = Backoff::default();
652 assert_eq!(backoff.initial_interval, Duration::from_millis(10));
653 assert_eq!(backoff.multiplier, 2.0);
654 assert_eq!(backoff.max_retries, 3);
655 }
656
657 #[test]
658 fn test_backoff_instance_exhaustion() {
659 let backoff = Backoff::new(Duration::from_millis(100), 2.0, 3);
660 let mut instance = backoff.instance();
661
662 assert!(!instance.exhausted());
663 instance.attempts = 3;
664 assert!(instance.exhausted());
665 }
666
667 #[test]
668 fn test_backoff_instance_starts_from_initial() {
669 let backoff = Backoff::new(Duration::from_millis(100), 2.0, 3);
670 let instance = backoff.instance();
671 assert_eq!(instance.attempts, 0);
672 assert_eq!(instance.current_interval, Duration::from_millis(100));
673 }
674
675 #[test]
676 fn test_backoff_advance() {
677 let backoff = Backoff::new(Duration::from_millis(100), 2.0, 5);
678 let mut instance = backoff.instance();
679 assert_eq!(instance.current_interval, Duration::from_millis(100));
680
681 instance.advance();
682 assert_eq!(instance.attempts, 1);
683 assert_eq!(instance.current_interval, Duration::from_millis(200));
684
685 instance.advance();
686 assert_eq!(instance.attempts, 2);
687 assert_eq!(instance.current_interval, Duration::from_millis(400));
688 }
689
690 #[test]
691 fn test_backoff_advance_unbounded_growth() {
692 let backoff = Backoff::new(Duration::from_millis(500), 2.0, 10);
693 let mut instance = backoff.instance();
694 instance.advance(); instance.advance(); assert_eq!(instance.current_interval, Duration::from_secs(2));
697 }
698
699 #[test]
700 fn test_backoff_iterator_yields_expected_intervals() {
701 let backoff = Backoff::new(Duration::from_millis(100), 2.0, 3);
702 let mut instance = backoff.instance();
703
704 assert_eq!(instance.next(), Some(Duration::from_millis(100)));
705 assert_eq!(instance.next(), Some(Duration::from_millis(200)));
706 assert_eq!(instance.next(), Some(Duration::from_millis(400)));
707 assert_eq!(instance.next(), None);
708 }
709
710 #[test]
711 fn test_backoff_iterator_stops_after_max_retries() {
712 let backoff = Backoff::new(Duration::from_millis(100), 2.0, 2);
713 let mut instance = backoff.instance();
714
715 assert_eq!(instance.next(), Some(Duration::from_millis(100)));
716 assert_eq!(instance.next(), Some(Duration::from_millis(200)));
717 assert_eq!(instance.next(), None);
718 assert_eq!(instance.next(), None);
719 }
720
721 #[tokio::test]
722 async fn test_backoff_retry_eventually_succeeds() {
723 let backoff = Backoff::new(Duration::from_millis(0), 2.0, 5);
724 let mut calls = 0;
725
726 let result = backoff
727 .retry(|| {
728 let current = calls;
729 calls += 1;
730 async move {
731 if current < 2 {
732 Err(ErrorCategory::Retryable("transient"))
733 } else {
734 Ok(42)
735 }
736 }
737 })
738 .await;
739
740 assert_eq!(result, Ok(42));
741 assert_eq!(calls, 3);
742 }
743
744 #[tokio::test]
745 async fn test_backoff_retry_exhausted_returns_last_error() {
746 let backoff = Backoff::new(Duration::from_millis(0), 2.0, 2);
747 let mut calls = 0;
748
749 let result = backoff
750 .retry(|| {
751 calls += 1;
752 async { Err::<(), _>(ErrorCategory::Retryable("still failing")) }
753 })
754 .await;
755
756 assert_eq!(result, Err("still failing"));
757 assert_eq!(calls, 3);
758 }
759
760 #[tokio::test]
761 async fn test_backoff_retry_with_zero_retries_does_not_retry() {
762 let backoff = Backoff::new(Duration::from_millis(0), 2.0, 0);
763 let mut calls = 0;
764
765 let result = backoff
766 .retry(|| {
767 calls += 1;
768 async { Err::<(), _>(ErrorCategory::Retryable("still failing")) }
769 })
770 .await;
771
772 assert_eq!(result, Err("still failing"));
773 assert_eq!(calls, 1);
774 }
775
776 #[test]
777 fn test_should_reconnect() {
778 assert!(is_recoverable_status_code(Code::Unavailable));
779 assert!(is_recoverable_status_code(Code::Internal));
780 assert!(is_recoverable_status_code(Code::Unknown));
781 assert!(is_recoverable_status_code(Code::Cancelled));
782 assert!(is_recoverable_status_code(Code::DeadlineExceeded));
783 assert!(is_recoverable_status_code(Code::ResourceExhausted));
784 assert!(is_recoverable_status_code(Code::Aborted));
785 assert!(is_recoverable_status_code(Code::DataLoss));
786 assert!(is_recoverable_status_code(Code::OutOfRange));
787
788 assert!(!is_recoverable_status_code(Code::InvalidArgument));
789 assert!(!is_recoverable_status_code(Code::NotFound));
790 assert!(!is_recoverable_status_code(Code::PermissionDenied));
791 assert!(!is_recoverable_status_code(Code::Unauthenticated));
792 assert!(!is_recoverable_status_code(Code::Unimplemented));
793 assert!(!is_recoverable_status_code(Code::FailedPrecondition));
794 }
795
796 #[test]
797 fn test_extract_slot_from_slot_update() {
798 let msg = SubscribeUpdate {
799 filters: vec![],
800 update_oneof: Some(UpdateOneof::Slot(SubscribeUpdateSlot {
801 slot: 42,
802 parent: None,
803 status: 0,
804 dead_error: None,
805 })),
806 created_at: None,
807 };
808 assert_eq!(extract_slot(&msg), Some(42));
809 }
810
811 #[test]
812 fn test_extract_slot_from_ping() {
813 let msg = SubscribeUpdate {
814 filters: vec![],
815 update_oneof: Some(UpdateOneof::Ping(SubscribeUpdatePing {})),
816 created_at: None,
817 };
818 assert_eq!(extract_slot(&msg), None);
819 }
820
821 #[test]
822 fn test_extract_slot_none() {
823 let msg = SubscribeUpdate {
824 filters: vec![],
825 update_oneof: None,
826 created_at: None,
827 };
828 assert_eq!(extract_slot(&msg), None);
829 }
830
831 fn make_slot_msg(slot: u64, status: i32) -> SubscribeUpdate {
832 SubscribeUpdate {
833 filters: vec![],
834 update_oneof: Some(UpdateOneof::Slot(SubscribeUpdateSlot {
835 slot,
836 parent: None,
837 status,
838 dead_error: None,
839 })),
840 created_at: None,
841 }
842 }
843
844 fn make_block_meta_msg(slot: u64) -> SubscribeUpdate {
845 SubscribeUpdate {
846 filters: vec![],
847 update_oneof: Some(UpdateOneof::BlockMeta(
848 yellowstone_grpc_proto::prelude::SubscribeUpdateBlockMeta {
849 slot,
850 blockhash: String::new(),
851 rewards: None,
852 block_time: None,
853 block_height: None,
854 parent_slot: slot.saturating_sub(1),
855 parent_blockhash: String::new(),
856 executed_transaction_count: 0,
857 entries_count: 0,
858 },
859 )),
860 created_at: None,
861 }
862 }
863
864 #[tokio::test]
865 async fn test_autoreconnect_recovers_from_recoverable_stream_error() {
866 let initial = stream::iter(vec![Err(Status::unavailable("disconnect"))]).boxed();
867 let connector = MockGrpcConnector::new(vec![ConnectPlan {
868 expected_from_slot: Some(None),
869 result: Ok(vec![Ok(make_slot_msg(42, 0))]),
870 }]);
871
872 let mut auto = AutoReconnect::new(
873 DedupStream::new(initial, DedupState::default()),
874 connector.clone(),
875 request_state(SubscribeRequest::default()),
876 backoff_with_retries(1),
877 );
878
879 let next = auto.next().await;
880 let slot = next
881 .expect("expected one item")
882 .expect("expected successful reconnect message");
883 assert_eq!(extract_slot(&slot), Some(42));
884 assert_eq!(connector.calls(), vec![None]);
885 }
886
887 #[tokio::test]
888 async fn test_autoreconnect_no_reconnect_when_max_retries_zero() {
889 let initial = stream::iter(vec![Err(Status::unavailable("disconnect"))]).boxed();
890 let connector = MockGrpcConnector::new(vec![]);
891
892 let mut auto = AutoReconnect::new(
893 DedupStream::new(initial, DedupState::default()),
894 connector.clone(),
895 request_state(SubscribeRequest::default()),
896 backoff_with_retries(0),
897 );
898
899 let first = auto.next().await.expect("expected one item");
900 assert!(first.is_err());
901 assert_eq!(
902 first.expect_err("expected recoverable error").code(),
903 Code::Unavailable
904 );
905 assert_eq!(connector.calls().len(), 0);
906 }
907
908 #[test]
909 fn test_status_codes_recoverable() {
910 for code in [
911 Code::Unavailable,
912 Code::Aborted,
913 Code::Internal,
914 Code::Unknown,
915 ] {
916 let status = Status::new(code, "test");
917 let err = GeyserGrpcClientError::TonicStatus(status);
918 assert!(
919 is_recoverable_client_error(&err),
920 "expected {:?} to be recoverable",
921 code
922 );
923 }
924 }
925
926 #[test]
927 fn test_status_codes_unrecoverable() {
928 for code in [
929 Code::InvalidArgument,
930 Code::PermissionDenied,
931 Code::Unauthenticated,
932 Code::NotFound,
933 ] {
934 let status = Status::new(code, "test");
935 let err = GeyserGrpcClientError::TonicStatus(status);
936 assert!(
937 !is_recoverable_client_error(&err),
938 "expected {:?} to be unrecoverable",
939 code
940 );
941 }
942 }
943
944 #[tokio::test]
945 async fn test_autoreconnect_passes_checkpoint_as_from_slot() {
946 let initial = stream::iter(vec![Err(Status::unavailable("disconnect"))]).boxed();
947 let connector = MockGrpcConnector::new(vec![ConnectPlan {
948 expected_from_slot: Some(Some(77)),
949 result: Ok(vec![Ok(make_slot_msg(90, 0))]),
950 }]);
951
952 let mut auto = AutoReconnect::new(
953 DedupStream::new(initial, DedupState::default()),
954 connector.clone(),
955 request_state(SubscribeRequest::default()),
956 backoff_with_retries(1),
957 );
958 auto.last_checkpoint = Some(77);
959
960 let msg = auto
961 .next()
962 .await
963 .expect("expected one item")
964 .expect("expected message after reconnect");
965 assert_eq!(extract_slot(&msg), Some(90));
966 assert_eq!(connector.calls(), vec![Some(77)]);
967 }
968
969 #[tokio::test]
970 async fn test_autoreconnect_dedup_survives_reconnect() {
971 let initial = stream::iter(vec![
972 Ok(make_slot_msg(100, 0)),
973 Err(Status::unavailable("disconnect")),
974 ])
975 .boxed();
976
977 let connector = MockGrpcConnector::new(vec![ConnectPlan {
978 expected_from_slot: None,
979 result: Ok(vec![
980 Ok(make_slot_msg(100, 0)), Ok(make_slot_msg(101, 0)), ]),
983 }]);
984
985 let mut auto = AutoReconnect::new(
986 DedupStream::new(initial, DedupState::default()),
987 connector.clone(),
988 request_state(SubscribeRequest::default()),
989 backoff_with_retries(1),
990 );
991
992 let msg1 = auto
993 .next()
994 .await
995 .expect("expected item")
996 .expect("expected ok");
997 assert_eq!(extract_slot(&msg1), Some(100));
998
999 let msg2 = auto
1000 .next()
1001 .await
1002 .expect("expected item")
1003 .expect("expected ok");
1004 assert_eq!(extract_slot(&msg2), Some(101));
1005 }
1006
1007 #[tokio::test]
1008 async fn test_autoreconnect_checkpoint_buffer() {
1009 let block_meta_msg = make_block_meta_msg(100);
1010
1011 let initial = stream::iter(vec![
1012 Ok(block_meta_msg),
1013 Err(Status::unavailable("disconnect")),
1014 ])
1015 .boxed();
1016
1017 let connector = MockGrpcConnector::new(vec![ConnectPlan {
1018 expected_from_slot: Some(Some(100 - CHECKPOINT_SLOT_BUFFER)),
1019 result: Ok(vec![Ok(make_slot_msg(105, 0))]),
1020 }]);
1021
1022 let mut auto = AutoReconnect::new(
1023 DedupStream::new(initial, DedupState::default()),
1024 connector.clone(),
1025 request_state(SubscribeRequest::default()),
1026 backoff_with_retries(1),
1027 );
1028
1029 let msg1 = auto
1030 .next()
1031 .await
1032 .expect("expected item")
1033 .expect("expected ok");
1034 assert_eq!(extract_slot(&msg1), Some(100));
1035
1036 let msg2 = auto
1037 .next()
1038 .await
1039 .expect("expected item")
1040 .expect("expected ok");
1041 assert_eq!(extract_slot(&msg2), Some(105));
1042 assert_eq!(connector.calls(), vec![Some(100 - CHECKPOINT_SLOT_BUFFER)]);
1043 }
1044
1045 #[tokio::test]
1046 async fn test_autoreconnect_unrecoverable_error_stops_stream() {
1047 let initial = stream::iter(vec![Err(Status::invalid_argument("bad filter"))]).boxed();
1048
1049 let connector = MockGrpcConnector::new(vec![]);
1050
1051 let mut auto = AutoReconnect::new(
1052 DedupStream::new(initial, DedupState::default()),
1053 connector.clone(),
1054 request_state(SubscribeRequest::default()),
1055 backoff_with_retries(1),
1056 );
1057
1058 let result = auto.next().await.expect("expected item");
1059 assert!(result.is_err());
1060 assert_eq!(
1061 result.expect_err("expected error").code(),
1062 Code::InvalidArgument
1063 );
1064
1065 assert!(auto.next().await.is_none());
1066 assert_eq!(connector.calls().len(), 0);
1067 }
1068
1069 #[tokio::test]
1070 async fn test_no_checkpoint_when_no_block_meta() {
1071 let account_msg = SubscribeUpdate {
1073 filters: vec![],
1074 update_oneof: Some(UpdateOneof::Account(SubscribeUpdateAccount {
1075 account: Some(SubscribeUpdateAccountInfo {
1076 pubkey: vec![1; 32],
1077 lamports: 100,
1078 owner: vec![0; 32],
1079 executable: false,
1080 rent_epoch: 0,
1081 data: vec![].into(),
1082 write_version: 1,
1083 txn_signature: Some(vec![0; 64]),
1084 }),
1085 slot: 100,
1086 is_startup: false,
1087 })),
1088 created_at: None,
1089 };
1090
1091 let initial = stream::iter(vec![
1092 Ok(account_msg),
1093 Err(Status::unavailable("disconnect")),
1094 ])
1095 .boxed();
1096
1097 let connector = MockGrpcConnector::new(vec![ConnectPlan {
1098 expected_from_slot: Some(None), result: Ok(vec![Ok(make_slot_msg(101, 0))]),
1100 }]);
1101
1102 let mut auto = AutoReconnect::new(
1103 DedupStream::new(initial, DedupState::default()),
1104 connector.clone(),
1105 request_state(SubscribeRequest::default()),
1106 backoff_with_retries(1),
1107 );
1108
1109 let _ = auto.next().await;
1111
1112 let _ = auto.next().await;
1114
1115 assert_eq!(
1116 connector.calls(),
1117 vec![None],
1118 "from_slot should be None when no block_meta received"
1119 );
1120 }
1121
1122 #[tokio::test]
1123 async fn test_checkpoint_updates_on_block_meta() {
1124 let block_meta_msg = SubscribeUpdate {
1125 filters: vec![],
1126 update_oneof: Some(UpdateOneof::BlockMeta(SubscribeUpdateBlockMeta {
1127 slot: 100,
1128 ..Default::default()
1129 })),
1130 created_at: None,
1131 };
1132
1133 let initial = stream::iter(vec![
1134 Ok(block_meta_msg),
1135 Err(Status::unavailable("disconnect")),
1136 ])
1137 .boxed();
1138
1139 let connector = MockGrpcConnector::new(vec![ConnectPlan {
1140 expected_from_slot: Some(Some(98)), result: Ok(vec![Ok(make_slot_msg(101, 0))]),
1142 }]);
1143
1144 let mut auto = AutoReconnect::new(
1145 DedupStream::new(initial, DedupState::default()),
1146 connector.clone(),
1147 request_state(SubscribeRequest::default()),
1148 backoff_with_retries(1),
1149 );
1150
1151 let _ = auto.next().await;
1153
1154 let _ = auto.next().await;
1156
1157 assert_eq!(
1158 connector.calls(),
1159 vec![Some(98)],
1160 "from_slot should be checkpoint - buffer"
1161 );
1162 }
1163
1164 #[tokio::test]
1165 async fn test_reconnect_exhausts_retries_then_stops() {
1166 let initial = stream::iter(vec![Err(Status::unavailable("disconnect"))]).boxed();
1167
1168 let connector = MockGrpcConnector::new(vec![
1170 ConnectPlan {
1171 expected_from_slot: Some(None),
1172 result: Err(GeyserGrpcClientError::TonicStatus(Status::unavailable(
1173 "still down",
1174 ))),
1175 },
1176 ConnectPlan {
1177 expected_from_slot: Some(None),
1178 result: Err(GeyserGrpcClientError::TonicStatus(Status::unavailable(
1179 "still down",
1180 ))),
1181 },
1182 ]);
1183
1184 let mut auto = AutoReconnect::new(
1185 DedupStream::new(initial, DedupState::default()),
1186 connector.clone(),
1187 request_state(SubscribeRequest::default()),
1188 backoff_with_retries(2), );
1190
1191 let result = auto.next().await;
1193 assert!(result.is_some());
1194 assert!(result.unwrap().is_err());
1195
1196 assert!(auto.next().await.is_none());
1198 }
1199}