@@ -20,6 +20,8 @@ use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient};
2020use yellowstone_grpc_proto:: prelude:: * ;
2121
2222const RATE_LIMIT_LOG_INTERVAL_SECS : u64 = 60 ;
23+ const INITIAL_RECONNECT_BACKOFF : Duration = Duration :: from_secs ( 1 ) ;
24+ const MAX_RECONNECT_BACKOFF : Duration = Duration :: from_secs ( 30 ) ;
2325
2426#[ derive( Debug , Clone ) ]
2527pub struct GeyserUpdate {
@@ -112,6 +114,7 @@ impl GeyserService {
112114 let tracked_accounts_vec: Vec < Pubkey > = self . tracked_accounts . keys ( ) . copied ( ) . collect ( ) ;
113115 let tls_config = ClientTlsConfig :: new ( ) . with_native_roots ( ) ;
114116 let mut from_slot: Option < u64 > = None ;
117+ let mut backoff = INITIAL_RECONNECT_BACKOFF ;
115118
116119 while !self . stop . load ( Ordering :: Relaxed ) {
117120 info ! ( "Connecting to Geyser..." ) ;
@@ -124,21 +127,43 @@ impl GeyserService {
124127
125128 // TODO: replace from_slot with auto-reconnect once we migrate to the up-to-date client (requires updating Solana deps):
126129 // https://docs.triton.one/project-yellowstone/dragons-mouth-grpc-subscriptions#auto-reconnect-rust-client
127- let mut client = self . tokio_rt . block_on (
128- GeyserGrpcClient :: build_from_shared ( self . endpoint . clone ( ) ) ?
130+ //
131+ // Establish the connection and subscription inside the reconnect loop so that a
132+ // transient Geyser outage (e.g. connection refused) triggers a retry with backoff
133+ // instead of propagating out of `start()` and panicking the whole process.
134+ let connect_result = self . tokio_rt . block_on ( async {
135+ let mut client = GeyserGrpcClient :: build_from_shared ( self . endpoint . clone ( ) ) ?
129136 . x_token ( self . x_token . clone ( ) ) ?
130137 . tls_config ( tls_config. clone ( ) ) ?
131- . connect ( ) ,
132- ) ?;
138+ . connect ( )
139+ . await ?;
140+ let ( _, stream) = client. subscribe_with_request ( Some ( sub_req. clone ( ) ) ) . await ?;
141+ Ok :: < _ , anyhow:: Error > ( stream)
142+ } ) ;
143+
144+ let mut stream = match connect_result {
145+ // Don't reset the backoff yet: a server can accept the connection and then
146+ // immediately reset the stream (REFUSED_STREAM). Only treat the connection as
147+ // healthy once it actually delivers a message (see below).
148+ Ok ( stream) => stream,
149+ Err ( e) => {
150+ self . error_logger . warn ( & format ! (
151+ "Failed to connect to Geyser, retrying in {:?}: {:?}" ,
152+ backoff, e
153+ ) ) ;
154+ self . sleep_interruptible ( backoff) ;
155+ backoff = ( backoff * 2 ) . min ( MAX_RECONNECT_BACKOFF ) ;
156+ continue ;
157+ }
158+ } ;
133159
134- let ( _, mut stream) = self
135- . tokio_rt
136- . block_on ( client. subscribe_with_request ( Some ( sub_req. clone ( ) ) ) ) ?;
137160 // TODO: use IndexerFlags
138161 info ! ( "Entering the GeyserService loop" ) ;
139162 while let Some ( msg) = self . tokio_rt . block_on ( stream. next ( ) ) {
140163 match msg {
141164 Ok ( msg) => {
165+ // A delivered message proves the connection is healthy: reset the backoff.
166+ backoff = INITIAL_RECONNECT_BACKOFF ;
142167 let update_oneof = ward ! ( msg. update_oneof, continue ) ;
143168 if let subscribe_update:: UpdateOneof :: Account ( account) = update_oneof {
144169 from_slot = Some ( account. slot ) ;
@@ -176,10 +201,15 @@ impl GeyserService {
176201 }
177202 Err ( error) => {
178203 self . error_logger . warn ( & format ! (
179- "Received error message from Geyser, reconnecting: {:?}" ,
180- error
204+ "Received error message from Geyser, reconnecting in {:?} : {:?}" ,
205+ backoff , error
181206 ) ) ;
182207
208+ // Back off before reconnecting so a server that keeps resetting the
209+ // stream isn't hammered in a tight loop.
210+ self . sleep_interruptible ( backoff) ;
211+ backoff = ( backoff * 2 ) . min ( MAX_RECONNECT_BACKOFF ) ;
212+
183213 // Break the inner loop so the outer loop reconnects.
184214 break ;
185215 }
@@ -196,6 +226,15 @@ impl GeyserService {
196226 Ok ( ( ) )
197227 }
198228
229+ /// Sleeps up to `duration`, waking early if a stop is requested so the reconnect
230+ /// backoff never delays a clean shutdown.
231+ fn sleep_interruptible ( & self , duration : Duration ) {
232+ let deadline = Instant :: now ( ) + duration;
233+ while Instant :: now ( ) < deadline && !self . stop . load ( Ordering :: Relaxed ) {
234+ std:: thread:: sleep ( Duration :: from_millis ( 200 ) . min ( deadline - Instant :: now ( ) ) ) ;
235+ }
236+ }
237+
199238 fn send_update ( & self , account_type : AccountType , address : Pubkey , account : & Account ) {
200239 let update = GeyserUpdate {
201240 account_type,
0 commit comments